ASS 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{ASS}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Howard Yeh"]
12
+ s.date = %q{2009-11-30}
13
+ s.email = %q{hayeah@gmail.com}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.textile"
17
+ ]
18
+ s.files = [
19
+ "ASS.gemspec",
20
+ "LICENSE",
21
+ "README.textile",
22
+ "Rakefile",
23
+ "VERSION.yml",
24
+ "lib/ass.rb",
25
+ "lib/ass/actor.rb",
26
+ "lib/ass/amqp.rb",
27
+ "lib/ass/callback_factory.rb",
28
+ "lib/ass/client.rb",
29
+ "lib/ass/peeper.rb",
30
+ "lib/ass/rpc.rb",
31
+ "lib/ass/server.rb",
32
+ "lib/ass/topic.rb",
33
+ "test/ass_test.rb",
34
+ "test/test_helper.rb"
35
+ ]
36
+ s.homepage = %q{http://github.com/hayeah/ass}
37
+ s.rdoc_options = ["--charset=UTF-8"]
38
+ s.require_paths = ["lib"]
39
+ s.rubygems_version = %q{1.3.5}
40
+ s.summary = %q{Asynchronous Service Stages for Distributed Services}
41
+ s.test_files = [
42
+ "spec/ass_spec.rb",
43
+ "spec/client_spec.rb",
44
+ "spec/actor_spec.rb",
45
+ "spec/rpc_spec.rb",
46
+ "test/ass_test.rb",
47
+ "test/test_helper.rb"
48
+ ]
49
+
50
+ if s.respond_to? :specification_version then
51
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
55
+ s.add_runtime_dependency(%q<amqp>, [">= 0"])
56
+ else
57
+ s.add_dependency(%q<amqp>, [">= 0"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<amqp>, [">= 0"])
61
+ end
62
+ end
63
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Howard Yeh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,162 @@
1
+
2
+ Asynchronous Service Stages (ASS) is a way to
3
+ organize distributed services by decoupling the
4
+ what and how of computation from the when and
5
+ where. Built on top of RabbitMQ (an implementation
6
+ of AMQP), ASS helps you to build robust and
7
+ scalable distributed applications.
8
+
9
+ End of project pimping, let's get started.
10
+
11
+ h1. Install
12
+
13
+ You need
14
+
15
+ (1) Erlang
16
+ (2) RabbitMQ
17
+ (3) AMQP gem
18
+
19
+ Thread.new { EM.run }
20
+ AMQP.start
21
+
22
+ ^C to exit
23
+
24
+
25
+ h1. The Basics
26
+
27
+ A service component is a ruby script that
28
+ communicates with RabbitMQ. You need to define the
29
+ AMQP server your ASS depends on. Something like this,
30
+
31
+
32
+ require 'rubygems'
33
+ require 'ass'
34
+ AMQP.start(:host => 'localhost',
35
+ #:vhost => "/ass-test",
36
+ :logging => false) do
37
+ # ASS definition
38
+ end
39
+
40
+
41
+ To start a server
42
+
43
+ server = ASS.new("echo")
44
+ # => #<ASS::Server echo>
45
+
46
+ But it doesn't do anything yet. You define the
47
+ behaviour of the server by setting its
48
+ callback. The callback can be a class, so that for
49
+ each client request an object is created from the
50
+ class to process the request. Like so,
51
+
52
+
53
+ server.react(SomeReactorClass)
54
+
55
+
56
+ However, often you just want something simple. The
57
+ react method can take a block and construct an
58
+ anonymous callback class from which the server
59
+ creates an callback object for each request. Here
60
+ we ask the server to react to @foo@ or @bar@.
61
+
62
+
63
+ server.react {
64
+ def foo(input)
65
+ [:server,:foo,input]
66
+ end
67
+
68
+ def oof(input)
69
+ [:server,:oof,input]
70
+ end
71
+ }
72
+
73
+
74
+ The react method accepts for the callback either
75
+ a Class, a Module, a block, or any object. When an
76
+ object is used, it's considered a singleton, which
77
+ is used to process all the requests.
78
+
79
+ Now that we have a server, we need to get a client
80
+ so to call the server. Because the call is
81
+ asynchronous (the client doesn't wait for the
82
+ result), to process the result when it gets back,
83
+ we need to define callback for the client (just as
84
+ we did for the server). For each call to the
85
+ remote server, the result is processed at the
86
+ client side by a method of the same name,
87
+
88
+
89
+ client = server.client.react {
90
+ def foo(output)
91
+ p [:client,:foo,output]
92
+ end
93
+ def oof(output)
94
+ p [:client,:oof,output]
95
+ end
96
+ }
97
+
98
+ c.call(:foo,42)
99
+ c.call(:oof,24)
100
+
101
+ # [:client,:foo,[:server,:foo,42]]
102
+ # [:client,:foo,[:server,:foo,24]]
103
+
104
+
105
+ > ruby server.rb
106
+ > ruby client.rb
107
+
108
+ > ruby server.rb
109
+ ^C
110
+
111
+ While the server is down, the requests the client
112
+ is making is queued by the underlying message
113
+ middleware (RabbitMQ), so in some future time when
114
+ we restart the server, we wouldn't lose any
115
+ request. Let's restart the server.
116
+
117
+ > ruby server.rb
118
+
119
+ See that the server caught up with all the pending
120
+ requests. To increase service capacity, we can
121
+ just increase the number of server instances.
122
+
123
+ > ruby server.rb
124
+
125
+ Now the load is distributed between these two
126
+ instances. We can also start more clients to
127
+ handle more load.
128
+
129
+ > ruby client.rb
130
+
131
+ You can see requests coming in from two clients.
132
+
133
+
134
+ h1. Service Configuration
135
+
136
+ -how the name of a service map to AMQP entities.
137
+ -various options for different functional characteristics.
138
+
139
+ -using routing_key
140
+ -using reply_to
141
+
142
+
143
+ rabbitmqctl list_exchanges
144
+ rabbitmqctl list_queues
145
+
146
+
147
+ h1. RPC service
148
+
149
+ The RPC client provides a synchronous API to the
150
+ asynchronous services. This is so the users of an
151
+ ASS application don't have to bother with the
152
+ difficulties of asynchronous programming style
153
+ with callbacks.
154
+
155
+ The RPC client is intended to be used as the
156
+ gateway into some reliable internal
157
+ services. While the internal services coordinated
158
+ with ASS needs to be robust to component failures,
159
+ there's no such requirements for gateways. It is
160
+ ok for a gateway to fail, and fail in delivering a
161
+ response, as long as the internal services carry
162
+ out the task without a hitch.
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "ASS"
8
+ gem.summary = "Asynchronous Service Stages for Distributed Services"
9
+ gem.email = "hayeah@gmail.com"
10
+ gem.homepage = "http://github.com/hayeah/ass"
11
+ gem.authors = ["Howard Yeh"]
12
+ gem.add_dependency "amqp"
13
+ gem.files = FileList["[A-Z]*", "{lib,test}/**/*"]
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ rescue LoadError
17
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/*_test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+
41
+ task :default => :test
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ if File.exist?('VERSION.yml')
46
+ config = YAML.load(File.read('VERSION.yml'))
47
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "ass #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
57
+
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -0,0 +1,112 @@
1
+ $:.unshift File.expand_path(File.dirname(File.expand_path(__FILE__)))
2
+ require 'mq'
3
+
4
+ module ASS; end
5
+ require 'ass/amqp' # monkey patch stolen from nanite.
6
+ require 'ass/server' # monkey patch stolen from nanite.
7
+ require 'ass/callback_factory'
8
+ require 'ass/actor'
9
+ require 'ass/rpc'
10
+ require 'ass/client'
11
+ # TODO a way to specify serializer (json, marshal...)
12
+ module ASS
13
+
14
+ class << self
15
+
16
+ def server(name,opts={},&block)
17
+ s = ASS::Server.new(name,opts)
18
+ if block
19
+ s.react(&block)
20
+ end
21
+ s
22
+ end
23
+
24
+ def actor(name,opts={},&block)
25
+ s = ASS::Actor.new(name,opts)
26
+ if block
27
+ s.react(&block)
28
+ end
29
+ s
30
+ end
31
+
32
+ def rpc(opts={})
33
+ ASS::RPC.new(opts)
34
+ end
35
+
36
+ # the opts is used to initiate an RPC
37
+ def client(opts={})
38
+ ASS::Client.new(opts)
39
+ end
40
+
41
+ #MQ = nil
42
+ def start(settings={})
43
+ raise "should have one ASS per eventmachine" if EM.reactor_running? == true # allow ASS to restart if EM is not running.
44
+ EM.run {
45
+ @mq = ::MQ.new(AMQP.start(settings))
46
+ # ASS and its worker threads (EM.threadpool) should share the same MQ instance.
47
+ yield if block_given?
48
+ }
49
+ end
50
+
51
+ def stop
52
+ AMQP.stop{ EM.stop }
53
+ true
54
+ end
55
+
56
+ def mq
57
+ @mq
58
+ end
59
+
60
+ def cast(name,method,data,opts,meta)
61
+ call(name,method,data,opts.merge(:reply_to => nil),meta)
62
+ end
63
+
64
+ def call(name,method,data,opts,meta)
65
+ payload = {
66
+ #:type => type,
67
+ :method => method,
68
+ :data => data,
69
+ :meta => meta,
70
+ }
71
+ payload.merge(:version => opts[:version]) if opts.has_key?(:version)
72
+ payload.merge(:meta => opts[:meta]) if opts.has_key?(:meta)
73
+ dummy_exchange(name).publish(::Marshal.dump(payload),opts)
74
+ true
75
+ end
76
+
77
+ # this would create a dummy MQ exchange object
78
+ # for the sole purpose of publishing the
79
+ # message. Will not clobber existing server
80
+ # already started in the process.
81
+ def dummy_exchange(name)
82
+ @mq.direct(name,:no_declare => true)
83
+ end
84
+
85
+ end
86
+
87
+
88
+ # def self.topic(name,opts={})
89
+ # ASS::Topic.new(name,opts)
90
+ # end
91
+
92
+
93
+
94
+
95
+ # def self.peep(server_name,callback=nil,&block)
96
+ # callback = block if callback.nil?
97
+ # callback = Module.new {
98
+ # def server(*args)
99
+ # p [:server,args]
100
+ # end
101
+
102
+ # def client(*args)
103
+ # p [:client,args]
104
+ # end
105
+ # }
106
+ # ASS::Peeper.new(server_name,callback)
107
+ # end
108
+
109
+ # assumes server initializes it with an exclusive and auto_delete queue.
110
+
111
+
112
+ end
@@ -0,0 +1,50 @@
1
+ class ASS::Actor
2
+ # this class is a thin layer over ASS::Server
3
+ def initialize(name,opts={},&block)
4
+ @server = ASS.server(name,opts)
5
+ if block
6
+ react(&block)
7
+ end
8
+ end
9
+
10
+ def queue(opts={})
11
+ @server.queue(opts)
12
+ self
13
+ end
14
+
15
+ def react(callback=nil,opts=nil,&block)
16
+ if block
17
+ opts = callback
18
+ callback = block
19
+ end
20
+ opts = {} if opts.nil?
21
+ callback_factory = ASS::CallbackFactory.new(callback)
22
+ server = @server # for closure capturing, needed for callback_factory
23
+ @server.react(opts) {
24
+ define_method(:on_call) do |_|
25
+ raise "can't call an actor with method set to nil" if payload[:method].nil?
26
+ callback_object = callback_factory.callback_for(server,header,payload)
27
+ callback_object.send(payload[:method],payload[:data])
28
+ end
29
+
30
+ define_method(:on_error) do |e,_|
31
+ callback_object = callback_factory.callback_for(server,header,payload)
32
+ if callback_object.respond_to?(:on_error)
33
+ callback_object.on_error(e,payload[:data])
34
+ else
35
+ raise e
36
+ end
37
+ end
38
+ }
39
+ self
40
+ end
41
+
42
+ def call(*args)
43
+ @server.call(*args)
44
+ end
45
+
46
+ def cast(*args)
47
+ @server.cast(*args)
48
+ end
49
+
50
+ end