dp_stm_map 0.0.2

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.
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"