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
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # DpStmMap
2
+
3
+ Implementation of a distributed and persistent Map with Software Transactional Memory (STM) semantics.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'dp_stm_map'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install dp_stm_map
19
+
20
+ ## Comprehensive defintion
21
+
22
+ Implementation of a distributed and persistent Map with Software Transactional Memory (STM) semantics.
23
+
24
+ ## What is it?
25
+
26
+ ### Implementation of a hash map (dp_stm_map) with ACID transactional updates.
27
+
28
+ ### Offers a subset of map (Hash) operations
29
+
30
+ :[], :[]= and :has_key?
31
+
32
+ ### Persistent
33
+
34
+ Unlike data stored in the Hash, data stored in dp_stm_map will survive a restart of RVM.
35
+
36
+ ### Distributed
37
+
38
+ More than one RVM can connect to a central transaction manager (dp_stm_manager) that coordinates transactions for the same dp_stm_map. Every RVM can have its own local view of the current state of the map, making reading operations of dp_stm_map very scalable.
39
+
40
+ ## What is so special about it?
41
+
42
+ Every RVM will receive notification on changes in the map. This makes dp_stm_map great foundation for
43
+ a distributed Event-driven architecture: Think of one RVM handling web traffic, another indexing persisted state and offering RESTful search service.
44
+
45
+ ## Usage
46
+
47
+ ### Manager
48
+ to start the central transaction manager execute:
49
+
50
+ $ dp_stm_manager.rb -s <storage directory> -p <port>
51
+
52
+ ### Client
53
+
54
+ require 'dp_stm_map'
55
+
56
+ client=DpStmMap::DistributedPersistentStmMap.new host,port,'storage'
57
+ client.start
58
+
59
+ #### Updating map within a transaction
60
+
61
+ client.atomic do |tx|
62
+ tx['key'] = 'value'
63
+ end
64
+
65
+ #### Read only transactions
66
+
67
+ value=client.atomic_read{ |tx| tx['key'] }
68
+
69
+ #### Adding a transaction listener
70
+
71
+ client.on_atomic do |change|
72
+ # change is map containing all value transitions of one transaction:
73
+ # e.g. {'key1' => [nil,'value1'], 'key2' => ['old_value_2','new_value2']}
74
+ change.each_pair do | k, (old_value, new_value) |
75
+ update_index k, new_value
76
+ end
77
+ end
78
+
79
+ #### Adding a transaction validator - will be executed before transaction is passed on to transaction manager
80
+
81
+ client.validate_atomic do |change|
82
+ # change is map containing all value transitions of one transaction:
83
+ # e.g. {'key1' => [nil,'value1'], 'key2' => ['old_value_2','new_value2']}
84
+ change.each_pair do | k, (old_value, new_value) |
85
+
86
+ # exception will abort (roll back) the transaction and will be raised as result of executing :atomic method
87
+ raise "new value is invalid" if new_value != 'valid_new_value'
88
+ end
89
+ end
90
+
91
+ ### Using object store wrapper (stores Ruby objects in dp_stm_map by serializing as YAML strings)
92
+
93
+ object_client=DpStmMap::ObjectStore.new client
94
+
95
+
96
+ # all transaction, notification and validation methods from dp_stm_map are available to ObjectStore
97
+
98
+ object_client.atomic do |tx|
99
+ user=User.new
100
+ user.username="admin"
101
+ tx[:user, "admin"]=user
102
+ end
103
+
104
+ user=object_client.atomic_read { |tx| tx[:user, "admin"] }
105
+
106
+ object_client.validate_atomic do |change|
107
+
108
+
109
+ ## Contributing
110
+
111
+ 1. Fork it
112
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
113
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
114
+ 4. Push to the branch (`git push origin my-new-feature`)
115
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # dp_stm_map - Distributed and Persistent Software Transaction Map
4
+ # Copyright (C) 2013 Dragan Milic
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+
16
+ require 'dp_stm_map'
17
+ require 'optparse'
18
+
19
+
20
+ port=0
21
+ store_dir=nil
22
+ OptionParser.new do |opts|
23
+ opts.banner == "Usage: dp_map_manager.rb -p <port>"
24
+ opts.on("-p N", Integer, "TCP port number where server should accept conntections") do |p|
25
+ port=p
26
+ end
27
+
28
+ opts.on("-s DIR", "Directory for storage") do |dir|
29
+ store_dir=dir
30
+ end
31
+
32
+ end.parse!
33
+
34
+ server=DpStmMap::Manager.new port, store_dir
35
+
36
+ server.start
37
+ puts "Manager started at port #{server.port}"
38
+ STDOUT.flush
39
+
40
+ $stdin.each do |line|
41
+ if /^quit/.match line
42
+ server.stop
43
+ puts "Manager shut down"
44
+ STDOUT.flush
45
+ exit(0)
46
+ end
47
+ end
data/cucumber.yml ADDED
@@ -0,0 +1,2 @@
1
+ autotest: features -r features --format pretty --color
2
+ autotest-all: features -r features --format pretty --color
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dp_stm_map/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dp_stm_map"
8
+ spec.version = DpStmMap::VERSION
9
+ spec.authors = ["Dragan Milic"]
10
+ spec.email = ["dragan@netice9.com"]
11
+ spec.description = %q{distributed and persistent software transaction memory map}
12
+ spec.summary = %q{distributed and persistent software transaction memory map}
13
+ spec.homepage = ""
14
+ spec.license = "GPLv3"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_development_dependency 'autotest'
25
+ spec.add_development_dependency 'cucumber'
26
+ spec.add_development_dependency 'aruba'
27
+ spec.add_dependency 'threadsafe-lru'
28
+ spec.add_dependency 'xray'
29
+ end
@@ -0,0 +1,13 @@
1
+ Feature: client reconnect
2
+
3
+ @announce
4
+ @announce-stderr
5
+ Scenario: Client should reconnect to the server when the server shuts down
6
+ Given I start dp_map_manager on port 31331 and store state for key "admin"
7
+ And I initialize first client connecting to port 31331
8
+ And I wait for 200ms
9
+ When I shut down dp_map_manager
10
+ And I start dp_map_manager on port 31331
11
+ And I wait for 700ms
12
+ Then the first client should have state for key "admin"
13
+ And the first client should be able to change state for key "admin"
@@ -0,0 +1,12 @@
1
+ Feature: persistence
2
+
3
+ @announce
4
+ @announce-stderr
5
+ Scenario: state remains persistent between server restarts
6
+ Given I start dp_map_manager on port 31331 and store state for key "admin"
7
+ And I shut down dp_map_manager
8
+ When I start server on port 31331
9
+ When I start second client that connects to port 31331
10
+ And I wait for 200ms
11
+ Then second client should have data for key "admin"
12
+
@@ -0,0 +1,9 @@
1
+ Feature: replication
2
+
3
+ Scenario: replication of state between two clients
4
+ Given I start dp_map_manager on port 31331
5
+ And I initialize two clients that connect to port 31331
6
+ When first client stores data for key "admin"
7
+ And I wait for 100ms
8
+ Then second client should have data for key "admin"
9
+
@@ -0,0 +1,19 @@
1
+ Feature: running manager
2
+
3
+ @announce
4
+ @announce-stderr
5
+ Scenario: starting and stopping server
6
+ Given I run `dp_map_manager.rb -p 31337 -s store` interactively
7
+ And I wait for stdout to contain "Manager started"
8
+ When I type "quit"
9
+ And I wait for stdout to contain "Manager shut down"
10
+ Then the exit status should be 0
11
+
12
+ @announce
13
+ @announce-stderr
14
+ Scenario: configuring port
15
+ When I start dp_map_manager on port 31331
16
+ Then server should be listening at port 31331
17
+
18
+
19
+
@@ -0,0 +1,28 @@
1
+ Given(/^I initialize first client connecting to port (\d+)$/) do |port|
2
+ @client1=create_client('localhost', port.to_i)
3
+ @client1.start
4
+ @client2=create_client('localhost', port.to_i)
5
+ @client2.start
6
+ sleep 0.2
7
+ @client1.atomic do |tx|
8
+ tx['admin']='other_value'
9
+ end
10
+ end
11
+
12
+ Then(/^the first client should have state for key "(.*?)"$/) do |key|
13
+ @client1.atomic_read do |tx|
14
+ tx.should have_key(key)
15
+ end
16
+ end
17
+
18
+ Then(/^the first client should be able to change state for key "(.*?)"$/) do |key|
19
+ @client1.atomic do |tx|
20
+ tx[key]='other_value'
21
+ end
22
+ sleep 0.2
23
+ puts "changed!"
24
+ @client1.atomic_read do |tx|
25
+ tx[key].should == 'other_value'
26
+ end
27
+
28
+ end
@@ -0,0 +1,49 @@
1
+ Given(/^I start dp_map_manager on port (\d+) and store state for key "(.*?)"$/) do |port, key|
2
+ run_interactive(unescape("dp_map_manager.rb -p #{port} -s store"))
3
+ wait_for_output("Manager started at port #{port}")
4
+ @server_port=port.to_i
5
+
6
+ @client1=create_client('localhost', @server_port)
7
+ @client1.start
8
+ @client1.atomic do |tx|
9
+ tx[key]='some value'
10
+ end
11
+
12
+ # @client1_socket=TCPSocket.new 'localhost', port.to_i
13
+ # send_message(@client1_socket, ClientHelloMessage.new(0))
14
+ # expect_message(@client1_socket, ServerHelloMessage)
15
+ # send_message(@client1_socket, ClientTransactionMessage.new('tx1',{key => [nil,'abc']},{'abc' => 'some value'}))
16
+ # success=expect_message(@client1_socket, ClientTransactionSuccessfulMessage)
17
+ # success.transaction_sequence.should_not be_nil
18
+ # success.transaction_id.should_not be_nil
19
+ # tx=expect_message(@client1_socket, TransactionMessage)
20
+ # tx.transaction_sequence.should == 1
21
+
22
+ end
23
+
24
+ Given(/^I shut down dp_map_manager$/) do
25
+ type('quit')
26
+ assert_exit_status(0)
27
+ end
28
+
29
+ When(/^I start server on port (\d+)$/) do |port|
30
+ @server_port=port.to_i
31
+ run_interactive(unescape("dp_map_manager.rb -p #{port} -s store"))
32
+ wait_for_output("Manager started at port #{port}")
33
+ end
34
+
35
+ When(/^I start second client that connects to port (\d+)$/) do |port|
36
+ @client2_socket=TCPSocket.new 'localhost', port
37
+ send_message(@client2_socket, ClientHelloMessage.new(0))
38
+ expect_message(@client2_socket, ServerHelloMessage)
39
+ end
40
+
41
+ Then(/^second client should have data for key "(.*?)"$/) do |key|
42
+ msg=expect_message(@client2_socket, TransactionMessage)
43
+ msg.transitions.should have_key(key)
44
+ end
45
+
46
+
47
+ When(/^I wait for (\d+)ms$/) do |ms|
48
+ sleep ms.to_f/1000.0
49
+ end
@@ -0,0 +1,15 @@
1
+ Given(/^I initialize two clients that connect to port (\d+)$/) do |port|
2
+ @client1_socket=TCPSocket.new 'localhost', port.to_i
3
+ send_message(@client1_socket, ClientHelloMessage.new(0))
4
+ expect_message(@client1_socket, ServerHelloMessage)
5
+
6
+ @client2_socket=TCPSocket.new 'localhost', port.to_i
7
+ send_message(@client2_socket, ClientHelloMessage.new(0))
8
+ expect_message(@client2_socket, ServerHelloMessage)
9
+ end
10
+
11
+ When(/^first client stores data for key "(.*?)"$/) do |key|
12
+ send_message(@client1_socket, ClientTransactionMessage.new('tx1',{key => [nil,'abc']},{'abc' => 'some value'}))
13
+ expect_message(@client1_socket, ClientTransactionSuccessfulMessage)
14
+ expect_message(@client1_socket, TransactionMessage)
15
+ end
@@ -0,0 +1,10 @@
1
+ When(/^I start dp_map_manager on port (\d+)$/) do |port|
2
+ @server_port=port.to_i
3
+ run_interactive(unescape("dp_map_manager.rb -p #{port} -s store"))
4
+ wait_for_output("Manager started")
5
+ end
6
+
7
+ Then(/^server should be listening at port (\d+)$/) do |port_s|
8
+ TCPSocket.new 'localhost', port_s.to_i
9
+ end
10
+
@@ -0,0 +1,11 @@
1
+ Given(/^client attempts to perform transaction that updates stale value$/) do
2
+ @client1_socket=TCPSocket.new 'localhost', @server_port
3
+ send_message(@client1_socket, ClientHelloMessage.new(0))
4
+ expect_message(@client1_socket, ServerHelloMessage)
5
+ send_message(@client1_socket, ClientTransactionMessage.new('tx1',{'a' => ['def','abc']},{'abc' => 'some value'}))
6
+ end
7
+
8
+ Then(/^client should be notified that transaction has failed$/) do
9
+ expect_message(@client1_socket, ClientTransactionFailedMessage)
10
+ end
11
+
@@ -0,0 +1,80 @@
1
+ $LOAD_PATH << File.expand_path('../../../lib',__FILE__)
2
+ require 'aruba/cucumber'
3
+ require 'dp_stm_map'
4
+ require 'tmpdir'
5
+ require 'timeout'
6
+
7
+ include DpStmMap
8
+
9
+
10
+ def wait_for_output expected
11
+ Timeout::timeout(exit_timeout) do
12
+ loop do
13
+ break if assert_partial_output_interactive(expected)
14
+ sleep 0.1
15
+ end
16
+ end
17
+ end
18
+
19
+
20
+
21
+ Before do
22
+ @tmpdirs=[]
23
+ @clients=[]
24
+
25
+ end
26
+
27
+ After do
28
+ # FileUtils.rm_rf @client_temp
29
+ @tmpdirs.each do |tmpdir|
30
+ FileUtils.rm_rf tmpdir
31
+ end
32
+
33
+ @clients.each do |client|
34
+ client.stop
35
+ end
36
+ end
37
+
38
+ After do
39
+ begin
40
+ type('quit')
41
+ assert_exit_status(0)
42
+ rescue
43
+
44
+ end
45
+ end
46
+
47
+
48
+ def make_temp_dir
49
+ tmpdir=Dir.mktmpdir
50
+ @tmpdirs << tmpdir
51
+ tmpdir
52
+ end
53
+
54
+
55
+ def send_message socket, message
56
+ yaml=message.serialize
57
+ socket.write([yaml.bytesize].pack("Q>"))
58
+ socket.write(yaml)
59
+ socket.flush
60
+ end
61
+
62
+ def expect_message socket, type
63
+ timeout(3) do
64
+ size=socket.read(8).unpack("Q>")[0]
65
+ message=JsonMessage::deserialize(socket.read(size))
66
+
67
+ # puts "message: #{message}"
68
+
69
+ message.should be_a type
70
+ message
71
+ end
72
+ end
73
+
74
+
75
+
76
+ def create_client host, port
77
+ client=DistributedPersistentStmMap.new host,port,make_temp_dir
78
+ @clients << client
79
+ client
80
+ end
@@ -0,0 +1,7 @@
1
+ Feature: failure of transaction
2
+
3
+
4
+ Scenario: updating stale data
5
+ Given I start dp_map_manager on port 31331
6
+ And client attempts to perform transaction that updates stale value
7
+ Then client should be notified that transaction has failed