probject 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'http://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'ichannel'
5
+ gem 'rspec'
6
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Johan Lundahl
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ probject
2
+ ========
3
+
4
+ A lightweight actor-based concurrent object framework with each object running in it's own process.
5
+
6
+ __How does it work?__
7
+
8
+ Each new object will fork and open up two UNIX sockets, one for requests and one for responses. Each method invocation on the actor is really asynchronous, but one can wait for the method to return by reading from the response channel.
9
+
10
+ __Example__
11
+
12
+ ```ruby
13
+ require 'net/http'
14
+ require 'probject'
15
+
16
+ class GoogleRequester < Probject::Actor
17
+
18
+ def do_request
19
+ @response = Net::HTTP.get('www.google.com', '/')
20
+ end
21
+
22
+ def response_length
23
+ @response.length
24
+ end
25
+ end
26
+
27
+ probjects = []
28
+
29
+ 1.upto 5 do |i|
30
+ probjects[i] = GoogleRequester.new
31
+
32
+ probjects[i].async.do_request
33
+ end
34
+
35
+ 1.upto 5 do |i|
36
+ puts probjects[i].response_length
37
+ end
38
+ ```
39
+
40
+ __Install__
41
+
42
+ $ gem install probject
43
+
44
+ __License__
45
+
46
+ Released under the MIT License. See `LICENSE.txt`
data/lib/probject.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'ichannel'
2
+ require 'timeout'
3
+ require 'securerandom'
4
+
5
+ require_relative 'probject/future'
6
+ require_relative 'probject/response_handler'
7
+ require_relative 'probject/proxy'
8
+ require_relative 'probject/actor'
@@ -0,0 +1,169 @@
1
+ module Probject
2
+
3
+ class TerminatedError < StandardError; end
4
+
5
+ class UnsupportedError < StandardError; end
6
+
7
+ class Actor
8
+
9
+ def initialize
10
+ @____request_channel = IChannel.new Marshal
11
+ response_channel = IChannel.new Marshal
12
+ @____response_handler = ResponseHandler.new response_channel
13
+ @____pid = Actor.spawn self, @____request_channel, response_channel
14
+
15
+ ObjectSpace.define_finalizer(self, self.class.finalize(@____pid))
16
+ end
17
+
18
+ def pid
19
+ @____pid
20
+ end
21
+
22
+ def terminate(timeout = 10)
23
+ raise_if_terminated!
24
+ @____terminated = true
25
+ if timeout && timeout != 0
26
+ begin
27
+ Timeout.timeout(timeout) do
28
+ Actor.send_signal(@____pid, 'SIGTERM')
29
+ end
30
+ rescue Timeout::Error
31
+ Actor.kill @____pid
32
+ end
33
+ else
34
+ Actor.kill @____pid
35
+ end
36
+ end
37
+
38
+ def terminated?
39
+ @____terminated
40
+ end
41
+
42
+ # asynchronous call
43
+ # returns nil
44
+ def async
45
+ raise_if_terminated!
46
+ Proxy.new(self, :async)
47
+ end
48
+ alias_method :tell, :async
49
+
50
+ # asynchronous call
51
+ # returns Probject::Future
52
+ def future
53
+ raise_if_terminated!
54
+ Proxy.new(self, :future)
55
+ end
56
+ alias_method :ask, :future
57
+
58
+ def self.finalize(pid)
59
+ proc { self.kill pid }
60
+ end
61
+
62
+ private
63
+
64
+ def raise_if_terminated!
65
+ if @____terminated
66
+ raise TerminatedError.new "Process #{@____pid} has been terminated!"
67
+ end
68
+ end
69
+
70
+ def self.kill(pid, timeout = 0.2)
71
+ begin
72
+ Timeout.timeout(timeout) do
73
+ Actor.send_signal(pid, 'SIGKILL')
74
+ end
75
+ rescue Timeout::Error
76
+ Process.detach pid
77
+ end
78
+ end
79
+
80
+ def self.send_signal(pid, signal)
81
+ begin
82
+ Process.kill signal, pid
83
+ Process.wait pid
84
+ rescue SystemCallError
85
+ end
86
+ end
87
+
88
+ def self.spawn(obj, request_channel, response_channel)
89
+ pid = fork do
90
+ termination_requested = false
91
+ trap :SIGTERM do
92
+ termination_requested = true
93
+ end
94
+ loop do
95
+ if request_channel.readable?
96
+ msg = request_channel.get
97
+ begin
98
+ method = obj.method("____#{msg[:name]}")
99
+ if msg[:block_given]
100
+ response = method.call *msg[:args] do |yielded|
101
+ response_channel.put id: msg[:id], type: :yield, value: yielded
102
+ end
103
+ else
104
+ response = method.call *msg[:args]
105
+ end
106
+ rescue Exception => e
107
+ response = e
108
+ end
109
+ if msg[:respond]
110
+ response_channel.put id: msg[:id], type: :return, value: response
111
+ end
112
+ elsif termination_requested
113
+ break
114
+ else
115
+ # wait a little before trying again
116
+ sleep 0.1
117
+ end
118
+ end
119
+ end
120
+ at_exit do
121
+ self.kill(pid)
122
+ end
123
+ pid
124
+ end
125
+
126
+ def self.method_added(name)
127
+ @added_methods ||= []
128
+ return if @added_methods.include? name.to_s
129
+ @added_methods += [name.to_s, "____#{name}", "____#{name}_async", "____#{name}_future"]
130
+
131
+ original_method = instance_method(name)
132
+
133
+ # original method is moved to ____name
134
+ define_method("____#{name}") do |*args, &block|
135
+ original_method.bind(self).call(*args, &block)
136
+ end
137
+
138
+ # async (tell)
139
+ define_method("____#{name}_async") do |*args, &block|
140
+ id = SecureRandom.uuid
141
+ if block
142
+ raise UnsupportedError.new "Cannot do asynchronous call when providing a block!"
143
+ end
144
+ @____request_channel.put id: id, respond: false, name: name, args: args, block_given: false
145
+ nil
146
+ end
147
+
148
+ # future (ask)
149
+ define_method("____#{name}_future") do |*args, &block|
150
+ id = SecureRandom.uuid
151
+ @____request_channel.put id: id, respond: true, name: name, args: args, block_given: !block.nil?
152
+ @____response_handler.register_block(id, block) if block
153
+ Future.new(@____response_handler, id)
154
+ end
155
+ ask = instance_method("____#{name}_future")
156
+
157
+ # original method or blocking call
158
+ # direct call or via channel depending on who is calling
159
+ define_method(name) do |*args, &block|
160
+ raise_if_terminated!
161
+ if @____pid # called from outside probject
162
+ ask.bind(self).call(*args, &block).get!
163
+ else # called from the probject itself
164
+ original_method.bind(self).call(*args, &block)
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,22 @@
1
+ module Probject
2
+ class Future
3
+ def initialize(response_handler, id)
4
+ @response_handler = response_handler
5
+ @id = id
6
+ end
7
+
8
+ def get
9
+ @response_handler.get @id
10
+ end
11
+
12
+ def get!
13
+ response = get
14
+ raise response if response.kind_of? Exception
15
+ response
16
+ end
17
+
18
+ def done?
19
+ @response_handler.done? @id
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module Probject
2
+ class Proxy
3
+
4
+ def initialize(probject, invocation_type)
5
+ @probject = probject
6
+ @invocation_type = invocation_type
7
+ end
8
+
9
+ def method_missing(name, *args, &block)
10
+ method = @probject.method("____#{name}_#{@invocation_type}")
11
+ method.call(*args, &block)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ module Probject
2
+ class ResponseHandler
3
+
4
+ def initialize(channel)
5
+ @channel = channel
6
+ @responses = {}
7
+ @blocks = {}
8
+ end
9
+
10
+ def get(id)
11
+ unless has_result? id
12
+ until get_result id
13
+ end
14
+ end
15
+ @responses.delete id
16
+ end
17
+
18
+ def done?(id)
19
+ has_result?(id) || get_results(id)
20
+ end
21
+
22
+ def register_block(id, block)
23
+ @blocks[id] = block
24
+ end
25
+
26
+ private
27
+
28
+ def has_result?(id)
29
+ @responses.include? id
30
+ end
31
+
32
+ def get_results(id)
33
+ while @channel.readable?
34
+ return true if get_result(id)
35
+ end
36
+ false
37
+ end
38
+
39
+ def get_result(id)
40
+ msg = @channel.get
41
+ if msg[:type] == :return # return
42
+ @responses[msg[:id]] = msg[:value]
43
+ @blocks.delete msg[:id] # no more yielding after return
44
+ return msg[:id] == id
45
+ else # yield
46
+ @blocks[msg[:id]].call msg[:value]
47
+ false
48
+ end
49
+ end
50
+ end
51
+ end
data/probject.gemspec ADDED
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'probject/version'
3
+
4
+ Gem::Specification.new 'probject', Probject::VERSION do |s|
5
+ s.summary = "Actor-based concurrent framework with objects running in separate processes"
6
+ s.description = "A lightweight actor-based concurrent object framework with each object running in it's own process"
7
+ s.authors = ['Johan Lundahl']
8
+ s.email = 'yohan.lundahl@gmail.com'
9
+ s.homepage = 'https://github.com/quezacoatl/probject'
10
+ s.files = `git ls-files`.split($\)
11
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
12
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
13
+
14
+ s.add_runtime_dependency 'ichannel', '~> 5.0.1'
15
+ end
@@ -0,0 +1,127 @@
1
+ require 'rspec'
2
+
3
+ require_relative '../lib/probject'
4
+
5
+ describe Probject::Actor do
6
+
7
+ before(:all) do
8
+ class SubProbject < Probject::Actor
9
+ attr_accessor :text
10
+
11
+ def say_hi
12
+ "Hi!"
13
+ end
14
+
15
+ def greet
16
+ "#{say_hi} How are you doing?"
17
+ end
18
+
19
+ def sleep_and_return(value = nil)
20
+ sleep 1
21
+ value
22
+ end
23
+
24
+ def using_block
25
+ yield "Hello block!"
26
+ yield "2"
27
+ yield "3"
28
+ end
29
+ end
30
+ @probject = SubProbject.new
31
+ end
32
+
33
+ def count_processes
34
+ Integer(`ps -ef | grep #{Process.pid} | grep -c -v grep`)
35
+ end
36
+
37
+ it "starts a child process" do
38
+ count_processes.should == 2
39
+ @probject.pid.should > Process.pid
40
+ end
41
+
42
+ it "handles simple method calls" do
43
+ @probject.say_hi.should == "Hi!"
44
+ end
45
+
46
+ it "can also use blocks" do
47
+ yielded = []
48
+ @probject.using_block do |y|
49
+ yielded << y
50
+ end
51
+ yielded.shift.should == "Hello block!"
52
+ yielded.shift.should == "2"
53
+ yielded.shift.should == "3"
54
+ end
55
+
56
+ it "handles method calls which invokes another method on self" do
57
+ @probject.greet.should == "Hi! How are you doing?"
58
+ end
59
+
60
+ context "sync" do
61
+ it "will block until response is received" do
62
+ @probject.sleep_and_return('test').should == 'test'
63
+ end
64
+ end
65
+
66
+ describe "#async" do
67
+ it "returns nil for asyncrhonous invocations" do
68
+ @probject.async.say_hi.should == nil
69
+ end
70
+
71
+ it "will not block" do
72
+ start = Time.now
73
+ @probject.async.sleep_and_return
74
+ (Time.now - start).should < 0.1
75
+ end
76
+
77
+ it "works to set attributes" do
78
+ @probject.async.text = '123'
79
+ @probject.text.should == '123'
80
+ end
81
+ end
82
+
83
+ describe "#future" do
84
+ it "knows if it is done" do
85
+ future = @probject.future.sleep_and_return
86
+ future.done?.should == false
87
+ sleep 1.1
88
+ future.done?.should == true
89
+ end
90
+
91
+ it "can block until it is done" do
92
+ @probject.future.sleep_and_return('test').get.should == 'test'
93
+ end
94
+
95
+ it "works to have several futures" do
96
+ a = @probject.future.sleep_and_return('a')
97
+ b = @probject.future.sleep_and_return('b')
98
+ c = @probject.future.sleep_and_return('c')
99
+ b.get.should == 'b'
100
+ c.get.should == 'c'
101
+ a.get.should == 'a'
102
+ end
103
+ end
104
+
105
+ context "terminated probject" do
106
+
107
+ before do
108
+ @probject = SubProbject.new
109
+ count_processes.should == 3 # yet another probject has been created here
110
+ @probject.terminate
111
+ end
112
+
113
+ it "terminated the child process" do
114
+ count_processes.should == 2
115
+ end
116
+
117
+ it "will raise exception on calls after terminated" do
118
+ expect {@probject.async.say_hi}.to raise_error Probject::TerminatedError
119
+ end
120
+
121
+ describe "#terminated?" do
122
+ it "returns true if probject has been terminated" do
123
+ @probject.terminated?.should == true
124
+ end
125
+ end
126
+ end
127
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: probject
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Johan Lundahl
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ichannel
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 5.0.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 5.0.1
30
+ description: A lightweight actor-based concurrent object framework with each object
31
+ running in it's own process
32
+ email: yohan.lundahl@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - Gemfile
39
+ - LICENSE.txt
40
+ - README.md
41
+ - lib/probject.rb
42
+ - lib/probject/actor.rb
43
+ - lib/probject/future.rb
44
+ - lib/probject/proxy.rb
45
+ - lib/probject/response_handler.rb
46
+ - probject.gemspec
47
+ - spec/probject_spec.rb
48
+ homepage: https://github.com/quezacoatl/probject
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.24
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Actor-based concurrent framework with objects running in separate processes
72
+ test_files:
73
+ - spec/probject_spec.rb