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.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +622 -0
- data/README.md +115 -0
- data/Rakefile +1 -0
- data/bin/dp_map_manager.rb +47 -0
- data/cucumber.yml +2 -0
- data/dp_stm_map.gemspec +29 -0
- data/features/client_reconnect.feature +13 -0
- data/features/persistence.feature +12 -0
- data/features/replication.feature +9 -0
- data/features/running_manager.feature +19 -0
- data/features/step_definitions/client_reconnect_steps.rb +28 -0
- data/features/step_definitions/persistence_steps.rb +49 -0
- data/features/step_definitions/replication_steps.rb +15 -0
- data/features/step_definitions/running_server_steps.rb +10 -0
- data/features/step_definitions/transaction_fail_steps.rb +11 -0
- data/features/support/env.rb +80 -0
- data/features/transaction_fail.feature +7 -0
- data/lib/dp_stm_map/Client.rb +268 -0
- data/lib/dp_stm_map/ClientLocalStore.rb +119 -0
- data/lib/dp_stm_map/InMemoryStmMap.rb +147 -0
- data/lib/dp_stm_map/Manager.rb +370 -0
- data/lib/dp_stm_map/Message.rb +126 -0
- data/lib/dp_stm_map/ObjectStore.rb +99 -0
- data/lib/dp_stm_map/version.rb +16 -0
- data/lib/dp_stm_map.rb +20 -0
- data/server.profile +547 -0
- data/spec/dp_stm_map/ClientLocalStore_spec.rb +78 -0
- data/spec/dp_stm_map/Client_spec.rb +133 -0
- data/spec/dp_stm_map/InMemoryStmMap_spec.rb +10 -0
- data/spec/dp_stm_map/Manager_spec.rb +323 -0
- data/spec/dp_stm_map/Message_spec.rb +21 -0
- data/spec/dp_stm_map/ObjectStore_spec.rb +87 -0
- data/spec/dp_stm_map/StmMap_shared.rb +432 -0
- 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
data/dp_stm_map.gemspec
ADDED
@@ -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
|