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