archipelago 0.1.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/GPL-2 +339 -0
- data/README +39 -0
- data/TODO +3 -0
- data/lib/archipelago.rb +24 -0
- data/lib/current.rb +128 -0
- data/lib/disco.rb +548 -0
- data/lib/hashish.rb +199 -0
- data/lib/pirate.rb +205 -0
- data/lib/tranny.rb +650 -0
- data/lib/treasure.rb +679 -0
- data/profiles/1000xChest#join!-prepare!-commit!.rb +19 -0
- data/profiles/1000xDubloon#[]=(t).rb +19 -0
- data/profiles/1000xDubloon#method_missing(t).rb +21 -0
- data/profiles/README +3 -0
- data/profiles/profile_helper.rb +25 -0
- data/scripts/chest.rb +20 -0
- data/scripts/console +3 -0
- data/scripts/pirate.rb +6 -0
- data/scripts/tranny.rb +20 -0
- data/tests/current_test.rb +28 -0
- data/tests/disco_test.rb +179 -0
- data/tests/pirate_test.rb +75 -0
- data/tests/test_helper.rb +60 -0
- data/tests/tranny_test.rb +70 -0
- data/tests/treasure_test.rb +257 -0
- metadata +69 -0
data/lib/hashish.rb
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Archipelago - a distributed computing toolkit for ruby
|
|
2
|
+
# Copyright (C) 2006 Martin Kihlgren <zond at troja dot ath dot cx>
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software
|
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
17
|
+
|
|
18
|
+
require 'current'
|
|
19
|
+
require 'bdb'
|
|
20
|
+
|
|
21
|
+
module Archipelago
|
|
22
|
+
|
|
23
|
+
#
|
|
24
|
+
# The module containing the persistence default provider.
|
|
25
|
+
#
|
|
26
|
+
module Hashish
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# In essence a Berkeley Database backed Hash.
|
|
30
|
+
#
|
|
31
|
+
# Will cache all values having been written or read
|
|
32
|
+
# in a normal Hash cache for fast access.
|
|
33
|
+
#
|
|
34
|
+
# Will save the last update timestamp for all keys
|
|
35
|
+
# in a separate Hash cache AND a separate Berkeley Database.
|
|
36
|
+
#
|
|
37
|
+
class BerkeleyHashish
|
|
38
|
+
include Archipelago::Current::Synchronized
|
|
39
|
+
#
|
|
40
|
+
# Initialize an instance with the +name+ and BDB::Env +env+.
|
|
41
|
+
#
|
|
42
|
+
def initialize(name, env)
|
|
43
|
+
super()
|
|
44
|
+
@content_db = env.open_db(BDB::HASH, name, "content", BDB::CREATE)
|
|
45
|
+
@content = {}
|
|
46
|
+
@timestamps_db = env.open_db(BDB::HASH, name, "timestamps", BDB::CREATE | BDB::NOMMAP)
|
|
47
|
+
@timestamps = {}
|
|
48
|
+
@lock = Archipelago::Current::Lock.new
|
|
49
|
+
end
|
|
50
|
+
#
|
|
51
|
+
# Returns a deep ( Marshal.load(Marshal.dump(o)) ) clone
|
|
52
|
+
# of the object represented by +key+.
|
|
53
|
+
#
|
|
54
|
+
def get_deep_clone(key)
|
|
55
|
+
return Marshal.load(@content_db[Marshal.dump(key)])
|
|
56
|
+
end
|
|
57
|
+
#
|
|
58
|
+
# Simply get the value for the +key+.
|
|
59
|
+
#
|
|
60
|
+
def [](key)
|
|
61
|
+
@lock.synchronize_on(key) do
|
|
62
|
+
|
|
63
|
+
value = @content[key]
|
|
64
|
+
return value if value
|
|
65
|
+
|
|
66
|
+
return get_from_db(key)
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
#
|
|
71
|
+
# Insert +value+ under +key+.
|
|
72
|
+
#
|
|
73
|
+
def []=(key, value)
|
|
74
|
+
@lock.synchronize_on(key) do
|
|
75
|
+
|
|
76
|
+
@content[key] = value
|
|
77
|
+
|
|
78
|
+
write_to_db(key, Marshal.dump(key), Marshal.dump(value))
|
|
79
|
+
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
#
|
|
85
|
+
# Stores whatever is under +key+ if it is not the same as
|
|
86
|
+
# whats in the persistent db.
|
|
87
|
+
#
|
|
88
|
+
def store_if_changed(key)
|
|
89
|
+
@lock.synchronize_on(key) do
|
|
90
|
+
|
|
91
|
+
serialized_key = Marshal.dump(key)
|
|
92
|
+
serialized_value = Marshal.dump(@content[key])
|
|
93
|
+
|
|
94
|
+
write_to_db(key, serialized_key, serialized_value) if @content_db[serialized_key] != serialized_value
|
|
95
|
+
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
#
|
|
99
|
+
# Returns the last time the value under +key+ was changed.
|
|
100
|
+
#
|
|
101
|
+
def timestamp(key)
|
|
102
|
+
@lock.synchronize_on(key) do
|
|
103
|
+
|
|
104
|
+
timestamp = @timestamps[key]
|
|
105
|
+
return timestamp if timestamp
|
|
106
|
+
|
|
107
|
+
serialized_key = Marshal.dump(key)
|
|
108
|
+
serialized_timestamp = @timestamps_db[serialized_key]
|
|
109
|
+
return nil unless serialized_timestamp
|
|
110
|
+
|
|
111
|
+
timestamp = Marshal.load(serialized_timestamp)
|
|
112
|
+
@timestamps[key] = timestamp
|
|
113
|
+
return timestamp
|
|
114
|
+
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
#
|
|
118
|
+
# Delete +key+ and its value and timestamp.
|
|
119
|
+
#
|
|
120
|
+
def delete(key)
|
|
121
|
+
@lock.synchronize_on(key) do
|
|
122
|
+
|
|
123
|
+
serialized_key = Marshal.dump(key)
|
|
124
|
+
|
|
125
|
+
@content.delete(key)
|
|
126
|
+
@content_db[serialized_key] = nil
|
|
127
|
+
@timestamps.delete(key)
|
|
128
|
+
@timestamps_db[serialized_key] = nil
|
|
129
|
+
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
#
|
|
136
|
+
# Write +key+, serialized as +serialized_key+ and
|
|
137
|
+
# +serialized_value+ to the db.
|
|
138
|
+
#
|
|
139
|
+
def write_to_db(key, serialized_key, serialized_value)
|
|
140
|
+
now = Time.now
|
|
141
|
+
|
|
142
|
+
@content_db[serialized_key] = serialized_value
|
|
143
|
+
@timestamps_db[serialized_key] = Marshal.dump(now)
|
|
144
|
+
@timestamps[key] = now
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
#
|
|
148
|
+
# Read +key+ from db and if it is found
|
|
149
|
+
# put it in the cache Hash.
|
|
150
|
+
#
|
|
151
|
+
def get_from_db(key)
|
|
152
|
+
serialized_key = Marshal.dump(key)
|
|
153
|
+
serialized_value = @content_db[serialized_key]
|
|
154
|
+
return nil unless serialized_value
|
|
155
|
+
|
|
156
|
+
value = Marshal.load(serialized_value)
|
|
157
|
+
@content[key] = value
|
|
158
|
+
return value
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
#
|
|
163
|
+
# A simple persistence provider backed by Berkeley db.
|
|
164
|
+
#
|
|
165
|
+
class BerkeleyHashishProvider
|
|
166
|
+
#
|
|
167
|
+
# Initialize an instance with the given
|
|
168
|
+
# +env_path+ to its database dir.
|
|
169
|
+
#
|
|
170
|
+
def initialize(env_path)
|
|
171
|
+
env_path.mkpath
|
|
172
|
+
@env = BDB::Env.open(env_path, BDB::CREATE | BDB::INIT_MPOOL)
|
|
173
|
+
end
|
|
174
|
+
#
|
|
175
|
+
# Returns a cleverly cached (but slightly inefficient)
|
|
176
|
+
# hash-like instance (see Archipelago::Hashish::BerkeleyHashish)
|
|
177
|
+
# using +name+.
|
|
178
|
+
#
|
|
179
|
+
def get_cached_hashish(name)
|
|
180
|
+
BerkeleyHashish.new(name, @env)
|
|
181
|
+
end
|
|
182
|
+
#
|
|
183
|
+
# Returns a normal hash-like instance using +name+.
|
|
184
|
+
#
|
|
185
|
+
def get_hashish(name)
|
|
186
|
+
@env.open_db(BDB::HASH, name, nil, BDB::CREATE | BDB::NOMMAP)
|
|
187
|
+
end
|
|
188
|
+
#
|
|
189
|
+
# Removes the persistent files of this instance.
|
|
190
|
+
#
|
|
191
|
+
def unlink
|
|
192
|
+
p = Pathname.new(@env.home)
|
|
193
|
+
p.rmtree if p.exist?
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
end
|
data/lib/pirate.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Archipelago - a distributed computing toolkit for ruby
|
|
2
|
+
# Copyright (C) 2006 Martin Kihlgren <zond at troja dot ath dot cx>
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software
|
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
17
|
+
|
|
18
|
+
require 'disco'
|
|
19
|
+
require 'tranny'
|
|
20
|
+
require 'treasure'
|
|
21
|
+
require 'pp'
|
|
22
|
+
require 'drb'
|
|
23
|
+
require 'digest/sha1'
|
|
24
|
+
|
|
25
|
+
module Archipelago
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# The client library that knows about
|
|
29
|
+
# all remote databases and handles reads
|
|
30
|
+
# and write to them.
|
|
31
|
+
#
|
|
32
|
+
module Pirate
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Raised when you try to begin a transaction but have no managers
|
|
36
|
+
# available.
|
|
37
|
+
#
|
|
38
|
+
class NoTransactionManagerAvailableException < RuntimeError
|
|
39
|
+
def initialize(pirate)
|
|
40
|
+
super("#{pirate} can not find any transaction manager for you")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#
|
|
45
|
+
# Raised when you try to do stuff without any remote database
|
|
46
|
+
# available.
|
|
47
|
+
#
|
|
48
|
+
class NoRemoteDatabaseAvailableException < RuntimeError
|
|
49
|
+
def initialize(pirate)
|
|
50
|
+
super("#{pirate} can not find any remote database for you")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
INITIAL_SERVICE_UPDATE_INTERVAL = 1
|
|
55
|
+
MAXIMUM_SERVICE_UPDATE_INTERVAL = 60
|
|
56
|
+
CHEST_DESCRIPTION = {
|
|
57
|
+
:class => 'Archipelago::Treasure::Chest'
|
|
58
|
+
}
|
|
59
|
+
TRANNY_DESCRIPTION = {
|
|
60
|
+
:class => 'Archipelago::Tranny::Manager'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#
|
|
64
|
+
# The class that actually keeps track of the Archipelago::Treasure:Chests and the
|
|
65
|
+
# Archipelago::Treasure:Dubloons in them.
|
|
66
|
+
#
|
|
67
|
+
class Captain
|
|
68
|
+
attr_reader :chests, :treasure_map, :trannies, :transaction
|
|
69
|
+
#
|
|
70
|
+
# Initialize an instance using an Archipelago::Disco::Jockey with <i>:jockey_options</i>
|
|
71
|
+
# if given, that looks for new services <i>:initial_service_update_interval</i> or INITIAL_SERVICE_UPDATE_INTERVAL,
|
|
72
|
+
# when it starts and never slower than every <i>:maximum_service_update_interval</i> or MAXIMUM_SERVICE_UPDATE_INTERVAL.
|
|
73
|
+
#
|
|
74
|
+
# Will look for Archipelago::Treasure::Chests matching <i>:chest_description</i> or CHEST_DESCRIPTION and
|
|
75
|
+
# Archipelago::Tranny::Managers matching <i>:tranny_description</i> or TRANNY_DESCRIPTION.
|
|
76
|
+
#
|
|
77
|
+
def initialize(options = {})
|
|
78
|
+
@treasure_map = Archipelago::Disco::Jockey.new(options[:jockey_options] || {})
|
|
79
|
+
|
|
80
|
+
@chest_description = CHEST_DESCRIPTION.merge(options[:chest_description] || {})
|
|
81
|
+
@tranny_description = TRANNY_DESCRIPTION.merge(options[:tranny_description] || {})
|
|
82
|
+
@jockey_options = options[:jockey_options] || {}
|
|
83
|
+
|
|
84
|
+
start_service_updater(options[:initial_service_update_interval] || INITIAL_SERVICE_UPDATE_INTERVAL,
|
|
85
|
+
options[:maximum_service_update_interval] || MAXIMUM_SERVICE_UPDATE_INTERVAL)
|
|
86
|
+
|
|
87
|
+
@yar_counter = 0
|
|
88
|
+
|
|
89
|
+
@transaction = nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#
|
|
93
|
+
# Get a value from the distributed database network using a +key+,
|
|
94
|
+
# optionally within a +transaction+.
|
|
95
|
+
#
|
|
96
|
+
def [](key, transaction = nil)
|
|
97
|
+
responsible_chest(key)[:service][key, transaction || @transaction]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
# Write a value to the distributed database network,
|
|
102
|
+
# optionally inside a transaction.
|
|
103
|
+
#
|
|
104
|
+
# Usage:
|
|
105
|
+
# * p["my key", transaction] = value
|
|
106
|
+
# * p["my key"] = value
|
|
107
|
+
#
|
|
108
|
+
def []=(key, p1, p2 = nil)
|
|
109
|
+
if @transaction && p2.nil?
|
|
110
|
+
p2 = p1
|
|
111
|
+
p1 = @transaction
|
|
112
|
+
end
|
|
113
|
+
responsible_chest(key)[:service][key, p1] = p2
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
#
|
|
117
|
+
# Delete a value from the distributed database network,
|
|
118
|
+
# optionally inside a transaction.
|
|
119
|
+
#
|
|
120
|
+
def delete(key, transaction = nil)
|
|
121
|
+
responsible_chest(key)[:service].delete(key, transaction || @transaction)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#
|
|
125
|
+
# Return a clone of this instance bound to a newly created transaction.
|
|
126
|
+
#
|
|
127
|
+
def begin
|
|
128
|
+
raise NoTransactionManagerAvailableException.new(self) if @trannies.empty?
|
|
129
|
+
|
|
130
|
+
rval = self.clone
|
|
131
|
+
rval.instance_eval do
|
|
132
|
+
@transaction = @trannies.values.first[:service].begin
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
return rval
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
#
|
|
139
|
+
# Commit the transaction we are a member of and forget about it.
|
|
140
|
+
#
|
|
141
|
+
def commit!
|
|
142
|
+
@transaction.commit!
|
|
143
|
+
@transaction = nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
#
|
|
147
|
+
# Abort the transaction we are a member of and forget about it.
|
|
148
|
+
#
|
|
149
|
+
def abort!
|
|
150
|
+
@transaction.abort!
|
|
151
|
+
@transaction = nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
#
|
|
155
|
+
# Yarrr!
|
|
156
|
+
#
|
|
157
|
+
def yar!
|
|
158
|
+
@yar_counter += 1
|
|
159
|
+
'yar!'
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
#
|
|
165
|
+
# Get the chest responsible for +key+.
|
|
166
|
+
#
|
|
167
|
+
def responsible_chest(key)
|
|
168
|
+
raise NoRemoteDatabaseAvailableException.new(self) if @chests.empty?
|
|
169
|
+
|
|
170
|
+
key_id = Digest::SHA1.new(Marshal.dump(key)).to_s
|
|
171
|
+
sorted_chest_ids = @chests.keys.sort
|
|
172
|
+
sorted_chest_ids.each do |id|
|
|
173
|
+
return @chests[id] if id > key_id
|
|
174
|
+
end
|
|
175
|
+
return @chests[sorted_chest_ids.first]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
#
|
|
179
|
+
# Start a thread looking up existing chests between every
|
|
180
|
+
# +initial+ and +maximum+ seconds.
|
|
181
|
+
#
|
|
182
|
+
def start_service_updater(initial, maximum)
|
|
183
|
+
@chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0)
|
|
184
|
+
@trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0)
|
|
185
|
+
Thread.start do
|
|
186
|
+
standoff = initial
|
|
187
|
+
loop do
|
|
188
|
+
begin
|
|
189
|
+
sleep(standoff)
|
|
190
|
+
standoff *= 2
|
|
191
|
+
standoff = maximum if standoff > maximum
|
|
192
|
+
@chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0)
|
|
193
|
+
@trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0)
|
|
194
|
+
rescue Exception => e
|
|
195
|
+
puts e
|
|
196
|
+
pp e.backtrace
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
end
|