odba 1.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.
- data/History.txt +5 -0
- data/LICENSE +459 -0
- data/Manifest.txt +38 -0
- data/README.txt +32 -0
- data/Rakefile +28 -0
- data/install.rb +1098 -0
- data/lib/odba.rb +72 -0
- data/lib/odba/18_19_loading_compatibility.rb +71 -0
- data/lib/odba/cache.rb +603 -0
- data/lib/odba/cache_entry.rb +122 -0
- data/lib/odba/connection_pool.rb +87 -0
- data/lib/odba/drbwrapper.rb +88 -0
- data/lib/odba/id_server.rb +26 -0
- data/lib/odba/index.rb +395 -0
- data/lib/odba/index_definition.rb +24 -0
- data/lib/odba/marshal.rb +18 -0
- data/lib/odba/odba.rb +45 -0
- data/lib/odba/odba_error.rb +12 -0
- data/lib/odba/persistable.rb +621 -0
- data/lib/odba/storage.rb +628 -0
- data/lib/odba/stub.rb +187 -0
- data/sql/collection.sql +6 -0
- data/sql/create_tables.sql +6 -0
- data/sql/object.sql +8 -0
- data/sql/object_connection.sql +7 -0
- data/test/suite.rb +8 -0
- data/test/test_array.rb +108 -0
- data/test/test_cache.rb +725 -0
- data/test/test_cache_entry.rb +109 -0
- data/test/test_connection_pool.rb +77 -0
- data/test/test_drbwrapper.rb +102 -0
- data/test/test_hash.rb +144 -0
- data/test/test_id_server.rb +43 -0
- data/test/test_index.rb +371 -0
- data/test/test_marshal.rb +28 -0
- data/test/test_persistable.rb +625 -0
- data/test/test_storage.rb +829 -0
- data/test/test_stub.rb +226 -0
- metadata +134 -0
data/lib/odba.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#-- ODBA -- odba -- 13.05.2004 -- hwyss@ywesee.com rwaltert@ywesee.com mwalder@ywesee.com
|
3
|
+
# Copyright (C) 2004 Hannes Wyss
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
18
|
+
#
|
19
|
+
# ywesee - intellectual capital connected, Winterthurerstrasse 52, CH-8006 Z�rich, Switzerland
|
20
|
+
# hwyss@ywesee.com
|
21
|
+
#++
|
22
|
+
# = ODBA - Object DataBase Access
|
23
|
+
#
|
24
|
+
# ODBA is an unintrusive Object Cache system. It adresses the crosscutting
|
25
|
+
# concern of object storage by disconnecting and serializing objects into
|
26
|
+
# storage. All disconnected connections are replaced by instances of
|
27
|
+
# ODBA::Stub, thus enabling transparent object-loading.
|
28
|
+
#
|
29
|
+
# ODBA supports:
|
30
|
+
# * transparent loading of connected objects
|
31
|
+
# * index-vectors
|
32
|
+
# * transactions
|
33
|
+
# * transparently fetches Hash-Elements without loading the entire Hash
|
34
|
+
#
|
35
|
+
# == Example
|
36
|
+
# include 'odba'
|
37
|
+
#
|
38
|
+
# # connect default storage manager to a relational database
|
39
|
+
# ODBA.storage.dbi = ODBA::ConnectionPool.new('DBI::pg::database', 'user', 'pw')
|
40
|
+
#
|
41
|
+
# class Counter
|
42
|
+
# include ODBA::Persistable
|
43
|
+
# def initialize
|
44
|
+
# @pos = 0
|
45
|
+
# end
|
46
|
+
# def up
|
47
|
+
# @pos += 1
|
48
|
+
# self.odba_store
|
49
|
+
# @pos
|
50
|
+
# end
|
51
|
+
# def down
|
52
|
+
# @pos -= 1
|
53
|
+
# self.odba_store
|
54
|
+
# @pos
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# :main:lib/odba.rb
|
59
|
+
|
60
|
+
require 'odba/persistable'
|
61
|
+
require 'odba/storage'
|
62
|
+
require 'odba/cache'
|
63
|
+
require 'odba/stub'
|
64
|
+
require 'odba/marshal'
|
65
|
+
require 'odba/cache_entry'
|
66
|
+
require 'odba/odba_error'
|
67
|
+
require 'odba/index'
|
68
|
+
require 'odba/odba'
|
69
|
+
|
70
|
+
class Odba
|
71
|
+
VERSION = '1.0.0'
|
72
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'strscan'
|
5
|
+
|
6
|
+
if RUBY_VERSION >= '1.9'
|
7
|
+
def u str
|
8
|
+
str
|
9
|
+
end
|
10
|
+
class Date
|
11
|
+
def self._load(str)
|
12
|
+
scn = StringScanner.new str
|
13
|
+
a = []
|
14
|
+
while match = scn.get_byte
|
15
|
+
case match
|
16
|
+
when ":"
|
17
|
+
len = scn.get_byte
|
18
|
+
name = scn.scan /.{#{Marshal.load("\x04\bi#{len}")}}/
|
19
|
+
when "i"
|
20
|
+
int = scn.get_byte
|
21
|
+
size, = int.unpack('c')
|
22
|
+
if size > 1 && size < 5
|
23
|
+
size.times do
|
24
|
+
int << scn.get_byte
|
25
|
+
end
|
26
|
+
end
|
27
|
+
dump = "\x04\bi" << int
|
28
|
+
a.push Marshal.load(dump)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
ajd = of = sg = 0
|
33
|
+
if a.size == 3
|
34
|
+
num, den, sg = a
|
35
|
+
ajd = Rational(num,den)
|
36
|
+
ajd -= 1.to_r/2
|
37
|
+
else
|
38
|
+
num, den, of, sg = a
|
39
|
+
ajd = Rational(num,den)
|
40
|
+
end
|
41
|
+
new!(ajd, of, sg)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
class Encoding
|
45
|
+
class Character
|
46
|
+
class UTF8 < String
|
47
|
+
module Methods
|
48
|
+
end
|
49
|
+
## when loading Encoding::Character::UTF8 instances simply return
|
50
|
+
# an encoded String
|
51
|
+
def self._load data
|
52
|
+
str = Marshal.load(data)
|
53
|
+
str.force_encoding 'UTF-8'
|
54
|
+
str
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
else
|
60
|
+
class Date
|
61
|
+
def marshal_load a
|
62
|
+
@ajd, @of, @sg, = a
|
63
|
+
@__ca__ = {}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
class Rational
|
67
|
+
def marshal_load a
|
68
|
+
@numerator, @denominator, = a
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/odba/cache.rb
ADDED
@@ -0,0 +1,603 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#-- Cache -- odba -- 29.04.2004 -- hwyss@ywesee.com rwaltert@ywesee.com mwalder@ywesee.com
|
3
|
+
|
4
|
+
require 'singleton'
|
5
|
+
require 'date'
|
6
|
+
begin
|
7
|
+
require 'rubygems'
|
8
|
+
gem 'fastthread', '>=0.6.3'
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
require 'thread'
|
12
|
+
require 'drb'
|
13
|
+
|
14
|
+
module ODBA
|
15
|
+
class Cache
|
16
|
+
include Singleton
|
17
|
+
include DRb::DRbUndumped
|
18
|
+
CLEANER_PRIORITY = 0 # :nodoc:
|
19
|
+
CLEANING_INTERVAL = 5 # :nodoc:
|
20
|
+
attr_accessor :cleaner_step, :destroy_age, :retire_age, :debug
|
21
|
+
def initialize # :nodoc:
|
22
|
+
if(self::class::CLEANING_INTERVAL > 0)
|
23
|
+
start_cleaner
|
24
|
+
end
|
25
|
+
@retire_age = 300
|
26
|
+
@cache_mutex = Mutex.new
|
27
|
+
@deferred_indices = []
|
28
|
+
@fetched = Hash.new
|
29
|
+
@prefetched = Hash.new
|
30
|
+
@clean_prefetched = false
|
31
|
+
@cleaner_offset = 0
|
32
|
+
@prefetched_offset = 0
|
33
|
+
@cleaner_step = 500
|
34
|
+
@loading_stats = {}
|
35
|
+
@peers = []
|
36
|
+
end
|
37
|
+
# Returns all objects designated by _bulk_fetch_ids_ and registers
|
38
|
+
# _odba_caller_ for each of them. Objects which are not yet loaded are loaded
|
39
|
+
# from ODBA#storage.
|
40
|
+
def bulk_fetch(bulk_fetch_ids, odba_caller)
|
41
|
+
instances = []
|
42
|
+
loaded_ids = []
|
43
|
+
bulk_fetch_ids.each { |id|
|
44
|
+
if(entry = fetch_cache_entry(id))
|
45
|
+
entry.odba_add_reference(odba_caller)
|
46
|
+
instances.push(entry.odba_object)
|
47
|
+
loaded_ids.push(id)
|
48
|
+
end
|
49
|
+
}
|
50
|
+
bulk_fetch_ids -= loaded_ids
|
51
|
+
unless(bulk_fetch_ids.empty?)
|
52
|
+
rows = ODBA.storage.bulk_restore(bulk_fetch_ids)
|
53
|
+
instances += bulk_restore(rows, odba_caller)
|
54
|
+
end
|
55
|
+
instances
|
56
|
+
end
|
57
|
+
def bulk_restore(rows, odba_caller = nil) # :nodoc:
|
58
|
+
retrieved_objects= []
|
59
|
+
rows.each { |row|
|
60
|
+
obj_id = row.at(0)
|
61
|
+
dump = row.at(1)
|
62
|
+
odba_obj = fetch_or_restore(obj_id, dump, odba_caller)
|
63
|
+
retrieved_objects.push(odba_obj)
|
64
|
+
}
|
65
|
+
retrieved_objects
|
66
|
+
end
|
67
|
+
def clean # :nodoc:
|
68
|
+
now = Time.now
|
69
|
+
start = Time.now if(@debug)
|
70
|
+
@cleaned = 0
|
71
|
+
if(@debug)
|
72
|
+
puts "starting cleaning cycle"
|
73
|
+
$stdout.flush
|
74
|
+
end
|
75
|
+
retire_horizon = now - @retire_age
|
76
|
+
@cleaner_offset = _clean(retire_horizon, @fetched, @cleaner_offset)
|
77
|
+
if(@clean_prefetched)
|
78
|
+
@prefetched_offset = _clean(retire_horizon, @prefetched,
|
79
|
+
@prefetched_offset)
|
80
|
+
end
|
81
|
+
if(@debug)
|
82
|
+
puts "cleaned: #{@cleaned} objects in #{Time.now - start} seconds"
|
83
|
+
puts "remaining objects in @fetched: #{@fetched.size}"
|
84
|
+
puts "remaining objects in @prefetched: #{@prefetched.size}"
|
85
|
+
mbytes = File.read("/proc/#{$$}/stat").split(' ').at(22).to_i / (2**20)
|
86
|
+
GC.start
|
87
|
+
puts "remaining objects in ObjectSpace: #{ObjectSpace.each_object {}}"
|
88
|
+
puts "memory-usage: #{mbytes}MB"
|
89
|
+
$stdout.flush
|
90
|
+
end
|
91
|
+
end
|
92
|
+
def _clean(retire_time, holder, offset) # :nodoc:
|
93
|
+
if(offset > holder.size)
|
94
|
+
offset = 0
|
95
|
+
end
|
96
|
+
counter = 0
|
97
|
+
cutoff = offset + @cleaner_step
|
98
|
+
holder.each_value { |value|
|
99
|
+
counter += 1
|
100
|
+
if(counter > offset && value.odba_old?(retire_time))
|
101
|
+
value.odba_retire && @cleaned += 1
|
102
|
+
end
|
103
|
+
return cutoff if(counter > cutoff)
|
104
|
+
}
|
105
|
+
cutoff
|
106
|
+
# every once in a while we'll get a 'hash modified during iteration'-Error.
|
107
|
+
# not to worry, we'll just try again later.
|
108
|
+
rescue StandardError
|
109
|
+
offset
|
110
|
+
end
|
111
|
+
# overrides the ODBA_PREFETCH constant and @odba_prefetch instance variable
|
112
|
+
# in Persistable. Use this if a secondary client is more memory-bound than
|
113
|
+
# performance-bound.
|
114
|
+
def clean_prefetched(flag=true)
|
115
|
+
if(@clean_prefetched = flag)
|
116
|
+
clean
|
117
|
+
end
|
118
|
+
end
|
119
|
+
def clear # :nodoc:
|
120
|
+
@fetched.clear
|
121
|
+
@prefetched.clear
|
122
|
+
end
|
123
|
+
# Creates or recreates automatically defined indices
|
124
|
+
def create_deferred_indices(drop_existing = false)
|
125
|
+
@deferred_indices.each { |definition|
|
126
|
+
name = definition.index_name
|
127
|
+
if(drop_existing && self.indices.include?(name))
|
128
|
+
drop_index(name)
|
129
|
+
end
|
130
|
+
unless(self.indices.include?(name))
|
131
|
+
index = create_index(definition)
|
132
|
+
if(index.target_klass.respond_to?(:odba_extent))
|
133
|
+
index.fill(index.target_klass.odba_extent)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
}
|
137
|
+
end
|
138
|
+
# Creates a new index according to IndexDefinition
|
139
|
+
def create_index(index_definition, origin_module=Object)
|
140
|
+
transaction {
|
141
|
+
klass = if(index_definition.fulltext)
|
142
|
+
FulltextIndex
|
143
|
+
elsif(index_definition.resolve_search_term.is_a?(Hash))
|
144
|
+
ConditionIndex
|
145
|
+
else
|
146
|
+
Index
|
147
|
+
end
|
148
|
+
index = klass.new(index_definition, origin_module)
|
149
|
+
indices.store(index_definition.index_name, index)
|
150
|
+
indices.odba_store_unsaved
|
151
|
+
index
|
152
|
+
}
|
153
|
+
end
|
154
|
+
# Permanently deletes _object_ from the database and deconnects all connected
|
155
|
+
# Persistables
|
156
|
+
def delete(odba_object)
|
157
|
+
odba_id = odba_object.odba_id
|
158
|
+
name = odba_object.odba_name
|
159
|
+
odba_object.odba_notify_observers(:delete, odba_id, odba_object.object_id)
|
160
|
+
rows = ODBA.storage.retrieve_connected_objects(odba_id)
|
161
|
+
rows.each { |row|
|
162
|
+
id = row.first
|
163
|
+
# Self-Referencing objects don't have to be resaved
|
164
|
+
begin
|
165
|
+
if(connected_object = fetch(id, nil))
|
166
|
+
connected_object.odba_cut_connection(odba_object)
|
167
|
+
connected_object.odba_isolated_store
|
168
|
+
end
|
169
|
+
rescue OdbaError
|
170
|
+
warn "OdbaError ### deleting #{odba_object.class}:#{odba_id}"
|
171
|
+
warn " ### while looking for connected object #{id}"
|
172
|
+
end
|
173
|
+
}
|
174
|
+
delete_cache_entry(odba_id)
|
175
|
+
delete_cache_entry(name)
|
176
|
+
ODBA.storage.delete_persistable(odba_id)
|
177
|
+
delete_index_element(odba_object)
|
178
|
+
odba_object
|
179
|
+
end
|
180
|
+
def delete_cache_entry(key)
|
181
|
+
@cache_mutex.synchronize {
|
182
|
+
@fetched.delete(key)
|
183
|
+
@prefetched.delete(key)
|
184
|
+
}
|
185
|
+
end
|
186
|
+
def delete_index_element(odba_object) # :nodoc:
|
187
|
+
klass = odba_object.class
|
188
|
+
indices.each_value { |index|
|
189
|
+
index.delete(odba_object)
|
190
|
+
}
|
191
|
+
end
|
192
|
+
# Permanently deletes the index named _index_name_
|
193
|
+
def drop_index(index_name)
|
194
|
+
transaction {
|
195
|
+
ODBA.storage.drop_index(index_name)
|
196
|
+
self.delete(self.indices[index_name])
|
197
|
+
}
|
198
|
+
end
|
199
|
+
def drop_indices # :nodoc:
|
200
|
+
keys = self.indices.keys
|
201
|
+
keys.each{ |key|
|
202
|
+
drop_index(key)
|
203
|
+
}
|
204
|
+
end
|
205
|
+
# Queue an index for creation by #setup
|
206
|
+
def ensure_index_deferred(index_definition)
|
207
|
+
@deferred_indices.push(index_definition)
|
208
|
+
end
|
209
|
+
# Get all instances of a class (- a limited extent)
|
210
|
+
def extent(klass, odba_caller=nil)
|
211
|
+
bulk_fetch(ODBA.storage.extent_ids(klass), odba_caller)
|
212
|
+
end
|
213
|
+
# Get number of instances of a class
|
214
|
+
def count(klass)
|
215
|
+
ODBA.storage.extent_count(klass)
|
216
|
+
end
|
217
|
+
# Fetch a Persistable identified by _odba_id_. Registers _odba_caller_ with
|
218
|
+
# the CacheEntry. Loads the Persistable if it is not already loaded.
|
219
|
+
def fetch(odba_id, odba_caller=nil)
|
220
|
+
fetch_or_do(odba_id, odba_caller) {
|
221
|
+
load_object(odba_id, odba_caller)
|
222
|
+
}
|
223
|
+
end
|
224
|
+
def fetch_cache_entry(odba_id_or_name) # :nodoc:
|
225
|
+
@prefetched[odba_id_or_name] || @fetched[odba_id_or_name]
|
226
|
+
end
|
227
|
+
@@receiver_name = RUBY_VERSION >= '1.9' ? :@receiver : '@receiver'
|
228
|
+
def fetch_collection(odba_obj) # :nodoc:
|
229
|
+
collection = []
|
230
|
+
bulk_fetch_ids = []
|
231
|
+
rows = ODBA.storage.restore_collection(odba_obj.odba_id)
|
232
|
+
return collection if rows.empty?
|
233
|
+
rows.each { |row|
|
234
|
+
key = ODBA.marshaller.load(row[0])
|
235
|
+
value = ODBA.marshaller.load(row[1])
|
236
|
+
item = nil
|
237
|
+
if([key, value].any? { |item| item.instance_variable_get(@@receiver_name) })
|
238
|
+
odba_id = odba_obj.odba_id
|
239
|
+
warn "stub for #{item.class}:#{item.odba_id} was saved with receiver in collection of #{odba_obj.class}:#{odba_id}"
|
240
|
+
warn "repair: remove [#{odba_id}, #{row[0]}, #{row[1].length}]"
|
241
|
+
ODBA.storage.collection_remove(odba_id, row[0])
|
242
|
+
key = key.odba_isolated_stub
|
243
|
+
key_dump = ODBA.marshaller.dump(key)
|
244
|
+
value = value.odba_isolated_stub
|
245
|
+
value_dump = ODBA.marshaller.dump(value)
|
246
|
+
warn "repair: insert [#{odba_id}, #{key_dump}, #{value_dump.length}]"
|
247
|
+
ODBA.storage.collection_store(odba_id, key_dump, value_dump)
|
248
|
+
end
|
249
|
+
bulk_fetch_ids.push(key.odba_id)
|
250
|
+
bulk_fetch_ids.push(value.odba_id)
|
251
|
+
collection.push([key, value])
|
252
|
+
}
|
253
|
+
bulk_fetch_ids.compact!
|
254
|
+
bulk_fetch_ids.uniq!
|
255
|
+
bulk_fetch(bulk_fetch_ids, odba_obj)
|
256
|
+
collection.each { |pair|
|
257
|
+
pair.collect! { |item|
|
258
|
+
if(item.is_a?(ODBA::Stub))
|
259
|
+
## don't fetch: that may result in a conflict when storing.
|
260
|
+
#fetch(item.odba_id, odba_obj)
|
261
|
+
item.odba_container = odba_obj
|
262
|
+
item
|
263
|
+
elsif(ce = fetch_cache_entry(item.odba_id))
|
264
|
+
warn "collection loaded unstubbed object: #{item.odba_id}"
|
265
|
+
ce.odba_add_reference(odba_obj)
|
266
|
+
ce.odba_object
|
267
|
+
else
|
268
|
+
item
|
269
|
+
end
|
270
|
+
}
|
271
|
+
}
|
272
|
+
collection
|
273
|
+
end
|
274
|
+
def fetch_collection_element(odba_id, key) # :nodoc:
|
275
|
+
key_dump = ODBA.marshaller.dump(key.odba_isolated_stub)
|
276
|
+
## for backward-compatibility and robustness we only attempt
|
277
|
+
## to load if there was a dump stored in the collection table
|
278
|
+
if(dump = ODBA.storage.collection_fetch(odba_id, key_dump))
|
279
|
+
item = ODBA.marshaller.load(dump)
|
280
|
+
if(item.is_a?(ODBA::Stub))
|
281
|
+
fetch(item.odba_id)
|
282
|
+
elsif(item.is_a?(ODBA::Persistable))
|
283
|
+
warn "collection_element was unstubbed object: #{item.odba_id}"
|
284
|
+
fetch_or_restore(item.odba_id, dump, nil)
|
285
|
+
else
|
286
|
+
item
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
def fetch_named(name, odba_caller, &block) # :nodoc:
|
291
|
+
fetch_or_do(name, odba_caller) {
|
292
|
+
dump = ODBA.storage.restore_named(name)
|
293
|
+
if(dump.nil?)
|
294
|
+
odba_obj = block.call
|
295
|
+
odba_obj.odba_name = name
|
296
|
+
odba_obj.odba_store(name)
|
297
|
+
odba_obj
|
298
|
+
else
|
299
|
+
fetch_or_restore(name, dump, odba_caller)
|
300
|
+
end
|
301
|
+
}
|
302
|
+
end
|
303
|
+
def fetch_or_do(obj_id, odba_caller, &block) # :nodoc:
|
304
|
+
if (cache_entry = fetch_cache_entry(obj_id)) && cache_entry._odba_object
|
305
|
+
cache_entry.odba_add_reference(odba_caller)
|
306
|
+
cache_entry.odba_object
|
307
|
+
else
|
308
|
+
block.call
|
309
|
+
end
|
310
|
+
end
|
311
|
+
def fetch_or_restore(odba_id, dump, odba_caller) # :nodoc:
|
312
|
+
fetch_or_do(odba_id, odba_caller) {
|
313
|
+
odba_obj, collection = restore(dump)
|
314
|
+
@cache_mutex.synchronize {
|
315
|
+
fetch_or_do(odba_id, odba_caller) {
|
316
|
+
cache_entry = CacheEntry.new(odba_obj)
|
317
|
+
cache_entry.odba_add_reference(odba_caller)
|
318
|
+
hash = odba_obj.odba_prefetch? ? @prefetched : @fetched
|
319
|
+
name = odba_obj.odba_name
|
320
|
+
hash.store(odba_obj.odba_id, cache_entry)
|
321
|
+
if name
|
322
|
+
hash.store(name, cache_entry)
|
323
|
+
end
|
324
|
+
odba_obj
|
325
|
+
}
|
326
|
+
}
|
327
|
+
}
|
328
|
+
end
|
329
|
+
def fill_index(index_name, targets)
|
330
|
+
self.indices[index_name].fill(targets)
|
331
|
+
end
|
332
|
+
# Checks wether the object identified by _odba_id_ has been loaded.
|
333
|
+
def include?(odba_id)
|
334
|
+
@fetched.include?(odba_id) || @prefetched.include?(odba_id)
|
335
|
+
end
|
336
|
+
def index_keys(index_name, length=nil)
|
337
|
+
index = indices.fetch(index_name)
|
338
|
+
index.keys(length)
|
339
|
+
end
|
340
|
+
def index_matches(index_name, substring, limit=nil, offset=0)
|
341
|
+
index = indices.fetch(index_name)
|
342
|
+
index.matches substring, limit, offset
|
343
|
+
end
|
344
|
+
# Returns a Hash-table containing all stored indices.
|
345
|
+
def indices
|
346
|
+
@indices ||= fetch_named('__cache_server_indices__', self) {
|
347
|
+
{}
|
348
|
+
}
|
349
|
+
end
|
350
|
+
def invalidate(odba_id)
|
351
|
+
## when finalizers are run, no other threads will be scheduled,
|
352
|
+
# therefore we don't need to @cache_mutex.synchronize
|
353
|
+
@fetched.delete odba_id
|
354
|
+
@prefetched.delete odba_id
|
355
|
+
end
|
356
|
+
def invalidate!(*odba_ids)
|
357
|
+
odba_ids.each do |odba_id|
|
358
|
+
if entry = fetch_cache_entry(odba_id)
|
359
|
+
entry.odba_retire :force => true
|
360
|
+
end
|
361
|
+
invalidate odba_id
|
362
|
+
end
|
363
|
+
end
|
364
|
+
# Returns the next valid odba_id
|
365
|
+
def next_id
|
366
|
+
id = ODBA.storage.next_id
|
367
|
+
@peers.each do |peer|
|
368
|
+
peer.reserve_next_id id rescue DRb::DRbError
|
369
|
+
end
|
370
|
+
id
|
371
|
+
rescue OdbaDuplicateIdError
|
372
|
+
retry
|
373
|
+
end
|
374
|
+
# Use this to load all prefetchable Persistables from the db at once
|
375
|
+
def prefetch
|
376
|
+
bulk_restore(ODBA.storage.restore_prefetchable)
|
377
|
+
end
|
378
|
+
# prints loading statistics to $stdout
|
379
|
+
def print_stats
|
380
|
+
fmh = " %-20s | %10s | %5s | %6s | %6s | %6s | %-20s\n"
|
381
|
+
fmt = " %-20s | %10.3f | %5i | %6.3f | %6.3f | %6.3f | %s\n"
|
382
|
+
head = sprintf(fmh,
|
383
|
+
"class", "total", "count", "min", "max", "avg", "callers")
|
384
|
+
line = "-" * head.length
|
385
|
+
puts line
|
386
|
+
print head
|
387
|
+
puts line
|
388
|
+
@loading_stats.sort_by { |key, val|
|
389
|
+
val[:total_time]
|
390
|
+
}.reverse.each { |key, val|
|
391
|
+
key = key.to_s
|
392
|
+
if(key.length > 20)
|
393
|
+
key = key[-20,20]
|
394
|
+
end
|
395
|
+
avg = val[:total_time] / val[:count]
|
396
|
+
printf(fmt, key, val[:total_time], val[:count],
|
397
|
+
val[:times].min, val[:times].max, avg,
|
398
|
+
val[:callers].join(','))
|
399
|
+
}
|
400
|
+
puts line
|
401
|
+
$stdout.flush
|
402
|
+
end
|
403
|
+
# Register a peer that has access to the same DB backend
|
404
|
+
def register_peer peer
|
405
|
+
@peers.push(peer).uniq!
|
406
|
+
end
|
407
|
+
# Reserve an id with all registered peers
|
408
|
+
def reserve_next_id id
|
409
|
+
ODBA.storage.reserve_next_id id
|
410
|
+
end
|
411
|
+
# Clears the loading statistics
|
412
|
+
def reset_stats
|
413
|
+
@loading_stats.clear
|
414
|
+
end
|
415
|
+
# Find objects in an index
|
416
|
+
def retrieve_from_index(index_name, search_term, meta=nil)
|
417
|
+
index = indices.fetch(index_name)
|
418
|
+
ids = index.fetch_ids(search_term, meta)
|
419
|
+
if meta.respond_to?(:error_limit) && (limit = meta.error_limit) \
|
420
|
+
&& (size = ids.size) > limit.to_i
|
421
|
+
error = OdbaResultLimitError.new
|
422
|
+
error.limit = limit
|
423
|
+
error.size = size
|
424
|
+
error.index = index_name
|
425
|
+
error.search_term = search_term
|
426
|
+
error.meta = meta
|
427
|
+
raise error
|
428
|
+
end
|
429
|
+
bulk_fetch(ids, nil)
|
430
|
+
end
|
431
|
+
# Create necessary DB-Structure / other storage-setup
|
432
|
+
def setup
|
433
|
+
ODBA.storage.setup
|
434
|
+
self.indices.each_key { |index_name|
|
435
|
+
ODBA.storage.ensure_target_id_index(index_name)
|
436
|
+
}
|
437
|
+
create_deferred_indices
|
438
|
+
nil
|
439
|
+
end
|
440
|
+
# Returns the total number of cached objects
|
441
|
+
def size
|
442
|
+
@prefetched.size + @fetched.size
|
443
|
+
end
|
444
|
+
def start_cleaner # :nodoc:
|
445
|
+
@cleaner = Thread.new {
|
446
|
+
Thread.current.priority = self::class::CLEANER_PRIORITY
|
447
|
+
loop {
|
448
|
+
sleep(self::class::CLEANING_INTERVAL)
|
449
|
+
begin
|
450
|
+
clean
|
451
|
+
rescue StandardError => e
|
452
|
+
puts e
|
453
|
+
puts e.backtrace
|
454
|
+
end
|
455
|
+
}
|
456
|
+
}
|
457
|
+
end
|
458
|
+
# Store a Persistable _object_ in the database
|
459
|
+
def store(object)
|
460
|
+
odba_id = object.odba_id
|
461
|
+
name = object.odba_name
|
462
|
+
object.odba_notify_observers(:store, odba_id, object.object_id)
|
463
|
+
if(ids = Thread.current[:txids])
|
464
|
+
ids.unshift([odba_id,name])
|
465
|
+
end
|
466
|
+
## get target_ids before anything else
|
467
|
+
target_ids = object.odba_target_ids
|
468
|
+
changes = store_collection_elements(object)
|
469
|
+
prefetchable = object.odba_prefetch?
|
470
|
+
dump = object.odba_isolated_dump
|
471
|
+
ODBA.storage.store(odba_id, dump, name, prefetchable, object.class)
|
472
|
+
store_object_connections(odba_id, target_ids)
|
473
|
+
update_references(target_ids, object)
|
474
|
+
object = store_cache_entry(odba_id, object, name)
|
475
|
+
update_indices(object)
|
476
|
+
@peers.each do |peer|
|
477
|
+
peer.invalidate! odba_id rescue DRb::DRbError
|
478
|
+
end
|
479
|
+
object
|
480
|
+
end
|
481
|
+
def store_cache_entry(odba_id, object, name=nil) # :nodoc:
|
482
|
+
@cache_mutex.synchronize {
|
483
|
+
if cache_entry = fetch_cache_entry(odba_id)
|
484
|
+
cache_entry.update object
|
485
|
+
else
|
486
|
+
hash = object.odba_prefetch? ? @prefetched : @fetched
|
487
|
+
cache_entry = CacheEntry.new(object)
|
488
|
+
hash.store(odba_id, cache_entry)
|
489
|
+
unless(name.nil?)
|
490
|
+
hash.store(name, cache_entry)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
cache_entry.odba_object
|
494
|
+
}
|
495
|
+
end
|
496
|
+
def store_collection_elements(odba_obj) # :nodoc:
|
497
|
+
odba_id = odba_obj.odba_id
|
498
|
+
collection = odba_obj.odba_collection.collect { |key, value|
|
499
|
+
[ ODBA.marshaller.dump(key.odba_isolated_stub),
|
500
|
+
ODBA.marshaller.dump(value.odba_isolated_stub) ]
|
501
|
+
}
|
502
|
+
old_collection = ODBA.storage.restore_collection(odba_id).collect { |row|
|
503
|
+
[row[0], row [1]]
|
504
|
+
}
|
505
|
+
changes = (old_collection - collection).each { |key_dump, _|
|
506
|
+
ODBA.storage.collection_remove(odba_id, key_dump)
|
507
|
+
}.size
|
508
|
+
changes + (collection - old_collection).each { |key_dump, value_dump|
|
509
|
+
ODBA.storage.collection_store(odba_id, key_dump, value_dump)
|
510
|
+
}.size
|
511
|
+
end
|
512
|
+
def store_object_connections(odba_id, target_ids) # :nodoc:
|
513
|
+
ODBA.storage.ensure_object_connections(odba_id, target_ids)
|
514
|
+
end
|
515
|
+
# Executes the block in a transaction. If the transaction fails, all
|
516
|
+
# affected Persistable objects are reloaded from the db (which by then has
|
517
|
+
# also performed a rollback). Rollback is quite inefficient at this time.
|
518
|
+
def transaction(&block)
|
519
|
+
Thread.current[:txids] = []
|
520
|
+
ODBA.storage.transaction(&block)
|
521
|
+
rescue Exception => excp
|
522
|
+
transaction_rollback
|
523
|
+
raise excp
|
524
|
+
ensure
|
525
|
+
Thread.current[:txids] = nil
|
526
|
+
end
|
527
|
+
def transaction_rollback # :nodoc:
|
528
|
+
if(ids = Thread.current[:txids])
|
529
|
+
ids.each { |id, name|
|
530
|
+
if(entry = fetch_cache_entry(id))
|
531
|
+
if(dump = ODBA.storage.restore(id))
|
532
|
+
odba_obj, collection = restore(dump)
|
533
|
+
entry.odba_replace!(odba_obj)
|
534
|
+
else
|
535
|
+
entry.odba_cut_connections!
|
536
|
+
delete_cache_entry(id)
|
537
|
+
delete_cache_entry(name)
|
538
|
+
end
|
539
|
+
end
|
540
|
+
}
|
541
|
+
end
|
542
|
+
end
|
543
|
+
# Unregister a peer
|
544
|
+
def unregister_peer peer
|
545
|
+
@peers.delete peer
|
546
|
+
end
|
547
|
+
def update_indices(odba_object) # :nodoc:
|
548
|
+
if(odba_object.odba_indexable?)
|
549
|
+
indices.each { |index_name, index|
|
550
|
+
index.update(odba_object)
|
551
|
+
}
|
552
|
+
end
|
553
|
+
end
|
554
|
+
def update_references(target_ids, object) # :nodoc:
|
555
|
+
target_ids.each { |odba_id|
|
556
|
+
if(entry = fetch_cache_entry(odba_id))
|
557
|
+
entry.odba_add_reference(object)
|
558
|
+
end
|
559
|
+
}
|
560
|
+
end
|
561
|
+
private
|
562
|
+
def load_object(odba_id, odba_caller)
|
563
|
+
start = Time.now if(@debug)
|
564
|
+
dump = ODBA.storage.restore(odba_id)
|
565
|
+
odba_obj = restore_object(odba_id, dump, odba_caller)
|
566
|
+
return odba_obj unless(@debug)
|
567
|
+
stats = (@loading_stats[odba_obj.class] ||= {
|
568
|
+
:count => 0, :times => [], :total_time => 0, :callers => [],
|
569
|
+
})
|
570
|
+
stats[:count] += 1
|
571
|
+
time = Time.now - start
|
572
|
+
stats[:times].push(time)
|
573
|
+
stats[:total_time] += time
|
574
|
+
stats[:callers].push(odba_caller.class).uniq!
|
575
|
+
if(time > 2)
|
576
|
+
names = []
|
577
|
+
odba_caller.instance_variables.each { |name|
|
578
|
+
if(odba_caller.instance_variable_get(name).odba_id == odba_id)
|
579
|
+
names.push(name)
|
580
|
+
end
|
581
|
+
}
|
582
|
+
printf("long load-time (%4.2fs) for odba_id %i: %s#%s\n",
|
583
|
+
time, odba_id, odba_caller, names.join(','))
|
584
|
+
end
|
585
|
+
odba_obj
|
586
|
+
end
|
587
|
+
def restore(dump)
|
588
|
+
odba_obj = ODBA.marshaller.load(dump)
|
589
|
+
unless(odba_obj.is_a?(Persistable))
|
590
|
+
odba_obj.extend(Persistable)
|
591
|
+
end
|
592
|
+
collection = fetch_collection(odba_obj)
|
593
|
+
odba_obj.odba_restore(collection)
|
594
|
+
[odba_obj, collection]
|
595
|
+
end
|
596
|
+
def restore_object(odba_id, dump, odba_caller)
|
597
|
+
if(dump.nil?)
|
598
|
+
raise OdbaError, "Unknown odba_id #{odba_id}"
|
599
|
+
end
|
600
|
+
fetch_or_restore(odba_id, dump, odba_caller)
|
601
|
+
end
|
602
|
+
end
|
603
|
+
end
|