odba 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|