dp_stm_map 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +622 -0
  5. data/README.md +115 -0
  6. data/Rakefile +1 -0
  7. data/bin/dp_map_manager.rb +47 -0
  8. data/cucumber.yml +2 -0
  9. data/dp_stm_map.gemspec +29 -0
  10. data/features/client_reconnect.feature +13 -0
  11. data/features/persistence.feature +12 -0
  12. data/features/replication.feature +9 -0
  13. data/features/running_manager.feature +19 -0
  14. data/features/step_definitions/client_reconnect_steps.rb +28 -0
  15. data/features/step_definitions/persistence_steps.rb +49 -0
  16. data/features/step_definitions/replication_steps.rb +15 -0
  17. data/features/step_definitions/running_server_steps.rb +10 -0
  18. data/features/step_definitions/transaction_fail_steps.rb +11 -0
  19. data/features/support/env.rb +80 -0
  20. data/features/transaction_fail.feature +7 -0
  21. data/lib/dp_stm_map/Client.rb +268 -0
  22. data/lib/dp_stm_map/ClientLocalStore.rb +119 -0
  23. data/lib/dp_stm_map/InMemoryStmMap.rb +147 -0
  24. data/lib/dp_stm_map/Manager.rb +370 -0
  25. data/lib/dp_stm_map/Message.rb +126 -0
  26. data/lib/dp_stm_map/ObjectStore.rb +99 -0
  27. data/lib/dp_stm_map/version.rb +16 -0
  28. data/lib/dp_stm_map.rb +20 -0
  29. data/server.profile +547 -0
  30. data/spec/dp_stm_map/ClientLocalStore_spec.rb +78 -0
  31. data/spec/dp_stm_map/Client_spec.rb +133 -0
  32. data/spec/dp_stm_map/InMemoryStmMap_spec.rb +10 -0
  33. data/spec/dp_stm_map/Manager_spec.rb +323 -0
  34. data/spec/dp_stm_map/Message_spec.rb +21 -0
  35. data/spec/dp_stm_map/ObjectStore_spec.rb +87 -0
  36. data/spec/dp_stm_map/StmMap_shared.rb +432 -0
  37. metadata +235 -0
@@ -0,0 +1,370 @@
1
+ # dp_stm_map - Distributed and Persistent Software Transaction Map
2
+ # Copyright (C) 2013 Dragan Milic
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (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
+ require 'socket'
15
+ require 'thread'
16
+ require 'set'
17
+ require 'fileutils'
18
+ require 'threadsafe-lru'
19
+
20
+ module DpStmMap
21
+
22
+ class ManagerShutdown < StandardError
23
+ end
24
+
25
+
26
+
27
+ class Manager
28
+
29
+ def initialize port, store_dir
30
+ @port=port
31
+ @state=:not_running
32
+
33
+
34
+ FileUtils.mkdir_p(store_dir) unless File.exist? store_dir
35
+
36
+ @transaction_log=TransactionLog.new("%s/log" % store_dir)
37
+ reference_counts=ReferenceCounts.new("%s/refcounts" % store_dir)
38
+ current_values=CurrentValues.new("%s/current_values" % store_dir)
39
+ @tx_manager=ClientTransactionManager.new(current_values, reference_counts, @transaction_log)
40
+ end
41
+
42
+ def port
43
+ @server_socket.addr[1]
44
+ end
45
+
46
+
47
+ def start
48
+ @server_socket=TCPServer.new @port
49
+ @accept_thread=Thread.new do
50
+ begin
51
+ active_threads=[]
52
+ loop do
53
+ client_socket=@server_socket.accept
54
+ client_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
55
+
56
+ active_threads << Thread.new do
57
+
58
+ begin
59
+ transport=SocketTransport.new(client_socket)
60
+ client_handler=ClientHandler.new transport, @tx_manager
61
+ msg=transport.next_message
62
+
63
+ raise "client did not send ClientHelloMessage" unless ClientHelloMessage === msg
64
+
65
+ transport.send_message ServerHelloMessage.new
66
+
67
+
68
+ @transaction_log.add_listener(msg.last_transaction_sequence) do |seq, tx|
69
+ client_handler.handle TransactionMessage.new(seq, *tx)
70
+ end
71
+
72
+ loop do
73
+ msg=transport.next_message
74
+ client_handler.handle msg
75
+ end
76
+
77
+ rescue ManagerShutdown => e
78
+
79
+
80
+ rescue => e
81
+ # puts "Error during processing: #{$!}"
82
+ # puts "Backtrace:\n\t#{e.backtrace.join("\n\t")}"
83
+
84
+ ensure
85
+ # puts "closed socket to client"
86
+ client_socket.close
87
+ end
88
+ end
89
+ end
90
+
91
+ rescue ManagerShutdown => e
92
+ ensure
93
+ @server_socket.close unless @server_socket.closed?
94
+ active_threads.each do |thread|
95
+ thread.raise ManagerShutdown
96
+ thread.join
97
+ end
98
+
99
+
100
+
101
+ end
102
+
103
+
104
+ end
105
+ end
106
+
107
+
108
+ def stop
109
+ @accept_thread.raise ManagerShutdown
110
+
111
+ begin
112
+ @accept_thread.join
113
+ rescue => e
114
+ end
115
+
116
+
117
+ end
118
+ end
119
+
120
+
121
+ class CurrentValues
122
+
123
+ def initialize current_values_dir
124
+ @current_values_dir=current_values_dir
125
+ FileUtils.mkdir_p(current_values_dir) unless File.exist?(current_values_dir)
126
+ @cache=ThreadSafeLru::LruCache.new 9000
127
+ end
128
+
129
+ def file_name key
130
+ "#{@current_values_dir}/#{key}"
131
+ end
132
+
133
+ def [] k
134
+ @cache.get(k) do |key|
135
+ File.exist?(file_name(key)) ? File.open(file_name(key),"r"){|f| f.read()} : nil
136
+ end
137
+ end
138
+
139
+ def []= key, value
140
+ if value
141
+ File.open(file_name(key),"w"){|f| f.write(value)}
142
+ else
143
+ File.unlink file_name(key) if File.exist? file_name(key)
144
+ end
145
+ @cache.drop(key)
146
+ end
147
+
148
+ end
149
+
150
+
151
+
152
+ class ReferenceCounts
153
+ def initialize references_dir
154
+ @references_dir=references_dir
155
+ FileUtils.mkdir_p(references_dir) unless File.exist?(references_dir)
156
+ @cache=ThreadSafeLru::LruCache.new 9000
157
+ end
158
+
159
+ def file_name ref
160
+ "#{@references_dir}/#{ref}"
161
+ end
162
+
163
+ def add_reference r
164
+ count=0
165
+ @cache.get(r) do |ref|
166
+ count=File.open(file_name(ref),"r") {|f| f.read().to_i} if File.exist? file_name(ref)
167
+ end
168
+ File.open(file_name(r),"w") {|f| f.write(count+1)}
169
+ @cache.drop(r)
170
+ end
171
+
172
+ def remove_reference ref
173
+ count=File.open(file_name(ref),"r") {|f| f.read().to_i}
174
+ File.open(file_name(ref),"w") {|f| f.write(count-1)}
175
+ @cache.drop(ref)
176
+ end
177
+
178
+ def has_references? r
179
+ count=0
180
+ @cache.get(r) do |ref|
181
+ count=File.open(file_name(ref),"r") {|f| f.read().to_i} if File.exist? file_name(ref)
182
+ end
183
+ count > 0
184
+ end
185
+
186
+ end
187
+
188
+ class TransactionLog
189
+
190
+ def initialize log_dir
191
+ @log_dir=log_dir
192
+
193
+ FileUtils.mkdir_p(log_dir) unless File.exist?(log_dir)
194
+
195
+ unless File.exist?("#{log_dir}/last_id.txt")
196
+ File.open("#{log_dir}/last_id.txt","w") {|f| f.write("0")}
197
+ @last_id=0
198
+ else
199
+ @last_id=File.open("#{log_dir}/last_id.txt","r") {|f| f.read().to_i}
200
+ end
201
+
202
+ @mutex=Mutex.new
203
+ @change=ConditionVariable.new
204
+ end
205
+
206
+
207
+ def read_tx id
208
+ JSON::parse(File.open("#{@log_dir}/#{id}.json","r") {|f| f.read})
209
+ end
210
+
211
+ def write_tx id, tx
212
+
213
+ File.open("#{@log_dir}/#{id}.json","w") {|f| f.write(tx.to_json)}
214
+ end
215
+
216
+
217
+
218
+ def add_listener current_state, &block
219
+ Thread.new do
220
+ begin
221
+ loop do
222
+ arg=@mutex.synchronize do
223
+ until current_state < last_id
224
+ @change.wait(@mutex)
225
+ end
226
+ current_state+=1
227
+ [current_state, read_tx(current_state)]
228
+ end
229
+ yield arg
230
+ end
231
+
232
+
233
+ rescue => e
234
+ # puts "Error during processing: #{$!}"
235
+ # puts "Backtrace:\n\t#{e.backtrace.join("\n\t")}"
236
+ end
237
+ end
238
+ end
239
+
240
+
241
+ def last_id
242
+ @last_id
243
+ end
244
+
245
+ def store_transaction references_to_change, values_to_add, values_to_remove
246
+ @mutex.synchronize do
247
+ txid=last_id+1
248
+ write_tx txid, [references_to_change, values_to_add, values_to_remove]
249
+ File.open("#{@log_dir}/last_id.txt","w") {|f| f.write(txid.to_s)}
250
+ @change.broadcast
251
+ @last_id=txid
252
+ txid
253
+ end
254
+ end
255
+
256
+
257
+ end
258
+
259
+ class SocketTransport
260
+ def initialize socket
261
+ @socket=socket
262
+ end
263
+
264
+ def next_message
265
+ length=@socket.read(8).unpack("Q>")[0]
266
+ JsonMessage::deserialize(@socket.read(length))
267
+ end
268
+
269
+ def send_message msg
270
+
271
+ data=msg.serialize
272
+ @socket.write([data.bytesize].pack("Q>"))
273
+ @socket.write(data)
274
+ @socket.flush
275
+ end
276
+
277
+ def close
278
+ @socket.close
279
+ end
280
+
281
+
282
+ end
283
+
284
+
285
+ class ClientHandler
286
+
287
+ def initialize client_transport, transaction_handler
288
+ @client_transport=client_transport
289
+ @transaction_handler=transaction_handler
290
+ @mutex=Mutex.new
291
+ end
292
+
293
+ def handle message
294
+ @mutex.synchronize do
295
+ if TransactionMessage === message
296
+ @client_transport.send_message message
297
+ end
298
+ if ClientTransactionMessage === message
299
+ begin
300
+ sequence=@transaction_handler.apply_transaction message.transitions, message.new_content
301
+ @client_transport.send_message ClientTransactionSuccessfulMessage.new message.transaction_id, sequence
302
+ rescue StaleTransactionError => e
303
+ @client_transport.send_message ClientTransactionFailedMessage.new message.transaction_id, e.message
304
+ end
305
+ end
306
+ end
307
+ end
308
+
309
+ end
310
+
311
+
312
+ class StaleTransactionError < StandardError
313
+ end
314
+
315
+ class ClientTransactionManager
316
+
317
+ def initialize current_values, reference_counts, transaction_log
318
+ @current_values=current_values
319
+ @reference_counts=reference_counts
320
+ @transaction_log=transaction_log
321
+ end
322
+
323
+
324
+ def apply_transaction transitions, new_values
325
+
326
+ values_to_remove=Set.new
327
+ values_to_add={}
328
+ references_to_change={}
329
+
330
+ transitions.each do |k,(old,new)|
331
+ current=@current_values[k]
332
+ raise StaleTransactionError, "transaction changing stale state" if (current != old)
333
+
334
+ next if old == new
335
+
336
+ if new && !@reference_counts.has_references?(new)
337
+ unless new_values.has_key? new
338
+ raise StaleTransactionError, "referencing not existing data"
339
+ else
340
+ values_to_add[new]=new_values[new]
341
+ end
342
+
343
+ end
344
+ end
345
+
346
+ transitions.each do |k,(old,new)|
347
+ if (old != new)
348
+ values_to_remove << old
349
+ references_to_change[k]=new
350
+ @current_values[k]=new
351
+ @reference_counts.add_reference(new) if new
352
+ @reference_counts.remove_reference(old) if old
353
+ end
354
+ end
355
+
356
+ values_to_remove=values_to_remove.select {|v| v && !@reference_counts.has_references?(v)}
357
+
358
+ # unless references_to_change.empty?
359
+ @transaction_log.store_transaction references_to_change, values_to_add, values_to_remove.to_set.to_a
360
+ # end
361
+
362
+
363
+
364
+ end
365
+
366
+ end
367
+
368
+
369
+
370
+ end
@@ -0,0 +1,126 @@
1
+ # dp_stm_map - Distributed and Persistent Software Transaction Map
2
+ # Copyright (C) 2013 Dragan Milic
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (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
+ require 'json'
15
+
16
+ module DpStmMap
17
+
18
+ class JsonMessage
19
+
20
+ def self.deserialize string
21
+ # return YAML::load(string)
22
+
23
+ json=JSON.parse(string)
24
+ type=json.delete('type')
25
+ cls=class_from_string(type)
26
+ obj=cls.new
27
+ obj.from_hash! json
28
+ obj
29
+ end
30
+
31
+ def self.class_from_string(str)
32
+ str.split('::').inject(Object) do |mod, class_name|
33
+ mod.const_get(class_name)
34
+ end
35
+ end
36
+
37
+ def serialize
38
+ # return YAML::dump(self)
39
+ hash = {}
40
+ self.instance_variables.each do |var|
41
+ hash[var] = self.instance_variable_get var
42
+ end
43
+ hash['type']=self.class.name
44
+ hash.to_json
45
+ end
46
+
47
+ def from_hash! hash
48
+ hash.each do |var, val|
49
+ self.instance_variable_set var, val
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+
56
+
57
+ class ClientHelloMessage < JsonMessage
58
+
59
+ def initialize last_transaction_sequence=nil
60
+ @last_transaction_sequence=last_transaction_sequence
61
+ end
62
+
63
+ attr_accessor :last_transaction_sequence
64
+
65
+ end
66
+
67
+ class ServerHelloMessage < JsonMessage
68
+ end
69
+
70
+ class ClientTransactionOutcomeMessage < JsonMessage
71
+ def initialize transaction_id
72
+ @transaction_id=transaction_id
73
+ end
74
+ attr_accessor :transaction_id
75
+ end
76
+
77
+ class ClientTransactionSuccessfulMessage < ClientTransactionOutcomeMessage
78
+
79
+ def initialize transaction_id=nil, transaction_sequence=nil
80
+ super transaction_id
81
+ @transaction_sequence=transaction_sequence
82
+ end
83
+
84
+ attr_accessor :transaction_sequence
85
+
86
+ end
87
+
88
+
89
+ class ClientTransactionFailedMessage < ClientTransactionOutcomeMessage
90
+
91
+ def initialize transaction_id=nil, reason=nil
92
+ super transaction_id
93
+ @reason=reason
94
+ end
95
+
96
+ attr_accessor :reason
97
+ end
98
+
99
+ class ClientTransactionMessage < JsonMessage
100
+ def initialize transaction_id=nil, transitions=nil, new_content=nil
101
+ @transaction_id=transaction_id
102
+ @transitions=transitions
103
+ @new_content=new_content
104
+ end
105
+
106
+ attr_accessor :transaction_id
107
+ attr_accessor :transitions
108
+ attr_accessor :new_content
109
+ end
110
+
111
+ class TransactionMessage < JsonMessage
112
+ def initialize transaction_sequence=nil, transitions=nil, new_content=nil, delete_content=nil
113
+ @transaction_sequence=transaction_sequence
114
+ @transitions=transitions
115
+ @new_content=new_content
116
+ @delete_content=delete_content
117
+ end
118
+
119
+ attr_accessor :transaction_sequence
120
+ attr_accessor :transitions
121
+ attr_accessor :new_content
122
+ attr_accessor :delete_content
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,99 @@
1
+ # dp_stm_map - Distributed and Persistent Software Transaction Map
2
+ # Copyright (C) 2013 Dragan Milic
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (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
+ require 'yaml'
15
+
16
+ module DpStmMap
17
+
18
+ class ObjectTransactionWrapper
19
+ def initialize tx
20
+ @tx=tx
21
+ end
22
+
23
+ def []= domain, key, value
24
+ @tx[domain_key(domain,key)]=value == nil ? nil : YAML::dump(value)
25
+ end
26
+
27
+ def [] domain, key
28
+ raw=@tx[domain_key(domain,key)]
29
+ if (raw == nil)
30
+ nil
31
+ else
32
+ YAML::load(raw)
33
+ end
34
+ end
35
+
36
+ def has_key? domain, key
37
+ @tx.has_key?(domain_key(domain,key))
38
+ end
39
+
40
+ def domain_key domain, key
41
+ "%s:%s" % [domain.to_s, key.to_s]
42
+ end
43
+
44
+ end
45
+
46
+ class ObjectStore
47
+
48
+ def initialize stm_map
49
+ @stm_map=stm_map
50
+ end
51
+
52
+
53
+ def on_atomic
54
+ @stm_map.on_atomic do |changes|
55
+ yield changes_to_object_changes changes
56
+ end
57
+ end
58
+
59
+ def atomic timeout=nil
60
+ @stm_map.atomic(timeout) do |stx|
61
+ yield ObjectTransactionWrapper.new(stx)
62
+ end
63
+ end
64
+
65
+ def atomic_read
66
+ @stm_map.atomic_read do |stx|
67
+ yield ObjectTransactionWrapper.new(stx)
68
+ end
69
+ end
70
+
71
+ def validate_atomic
72
+ @stm_map.validate_atomic do |changes|
73
+ yield changes_to_object_changes changes
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+
80
+ def changes_to_object_changes changes
81
+ changes.inject({}) {|h, (k,v)| h[string_to_domain_key(k)]=[deserialize_object(v[0]),deserialize_object(v[1])]; h}
82
+ end
83
+
84
+
85
+ def string_to_domain_key str
86
+ parts=str.split(/:/)
87
+ [parts[0].to_sym, parts[1]]
88
+ end
89
+
90
+ def deserialize_object str
91
+ str == nil ? nil : YAML::load(str)
92
+ end
93
+
94
+ end
95
+
96
+
97
+
98
+
99
+ end
@@ -0,0 +1,16 @@
1
+ # dp_stm_map - Distributed and Persistent Software Transaction Map
2
+ # Copyright (C) 2013 Dragan Milic
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (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
+ module DpStmMap
15
+ VERSION = "0.0.2"
16
+ end
data/lib/dp_stm_map.rb ADDED
@@ -0,0 +1,20 @@
1
+ # dp_stm_map - Distributed and Persistent Software Transaction Map
2
+ # Copyright (C) 2013 Dragan Milic
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (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
+ require "dp_stm_map/version"
15
+ require "dp_stm_map/InMemoryStmMap"
16
+ require "dp_stm_map/Manager"
17
+ require "dp_stm_map/Message"
18
+ require "dp_stm_map/Client"
19
+ require "dp_stm_map/ClientLocalStore"
20
+ require "dp_stm_map/ObjectStore"