zack 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,15 @@
1
+ = 0.3.3 / 29Nov2011
2
+ + Updates to the cod rewrite, this eliminates the dependency on
3
+ beanstalk-client.
4
+
5
+ + Works and is designed only for beanstalk now. This allows usage of some
6
+ advanced queuing features.
7
+
8
+ + Breaking change: Default server is now 'localhost:11300', not a server on
9
+ the LAN we might not even own.
10
+
11
+ + Breaking change: Exception block now has two arguments.
12
+
1
13
  = 0.3.2 / 31Aug2011
2
14
 
3
15
  . gemspec fix
data/README CHANGED
@@ -40,7 +40,7 @@ above.
40
40
 
41
41
  DEPENDENCIES
42
42
 
43
- Depends on beanstalkd (beanstalkd 1.4.4 or better).
43
+ Depends on beanstalkd server (beanstalkd 1.4.4 or better).
44
44
 
45
45
  COMPATIBILITY
46
46
 
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require 'rubygems/package_task'
4
4
 
5
5
  require 'rspec'
6
6
  require 'rspec/core/rake_task'
7
- Rspec::Core::RakeTask.new
7
+ RSpec::Core::RakeTask.new
8
8
  task :default => :spec
9
9
 
10
10
  # This task actually builds the gem.
data/example/client.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  $:.unshift File.dirname(__FILE__) + "/../lib"
2
2
  require 'zack'
3
3
 
4
- client = Zack::Client.new('sample', :with_answer => [:get_time])
4
+ client = Zack::Client.new('sample',
5
+ :with_answer => [:get_time])
5
6
 
6
7
  client.announce
7
8
  puts client.get_time
data/example/server.rb CHANGED
@@ -12,5 +12,4 @@ end
12
12
 
13
13
  Zack::Server.new(
14
14
  'sample',
15
- :simple => ChunkyBaconAnnouncer,
16
- ).run
15
+ :simple => ChunkyBaconAnnouncer).run
data/lib/zack.rb CHANGED
@@ -8,10 +8,7 @@ module Zack
8
8
  class ServiceTimeout < StandardError; end
9
9
  end
10
10
 
11
- require 'zack/transparent_proxy'
12
11
  require 'zack/unique_name'
13
12
 
14
- require 'zack/target'
15
-
16
13
  require 'zack/server'
17
14
  require 'zack/client'
data/lib/zack/client.rb CHANGED
@@ -12,21 +12,19 @@ module Zack
12
12
  # :server :: beanstalkd server location url
13
13
  # :only :: ignores all messages not in this hash
14
14
  # :with_answer :: these messages wait for an answer from the service
15
+ # :timeout :: How long to wait for an answer
15
16
  #
16
17
  def initialize(tube_name, opts={})
17
- server = opts[:server] || 'beanstalk:11300'
18
18
  # Only respond to these messages
19
19
  @only = opts[:only] || proc { true }
20
20
  # These have answers (wait for the server to answer)
21
21
  @with_answer = opts[:with_answer] || []
22
+ @timeout = opts[:timeout]
23
+
24
+ @tube_name = tube_name
25
+ @server = opts[:server] || 'localhost:11300'
22
26
 
23
- @outgoing = Cod.beanstalk(server, tube_name)
24
- unless @with_answer.empty?
25
- @incoming = Cod.beanstalk(server,
26
- UniqueName.new(tube_name))
27
- end
28
-
29
- @service = Cod::Client.new(@outgoing, @incoming, 1)
27
+ connect
30
28
  end
31
29
 
32
30
  def respond_to?(msg)
@@ -37,6 +35,59 @@ module Zack
37
35
  @with_answer.include?(sym.to_sym)
38
36
  end
39
37
 
40
- include TransparentProxy
38
+ def method_missing(sym, *args, &block)
39
+ super unless respond_to?(sym)
40
+
41
+ raise ArgumentError, "Can't call methods remotely with a block" if block
42
+
43
+ if has_answer?(sym)
44
+ with_timeout do
45
+ return service.call([sym, args])
46
+ end
47
+ else
48
+ service.notify [sym, args]
49
+ return nil
50
+ end
51
+ rescue Timeout::Error => ex
52
+ raise Zack::ServiceTimeout, "The service took too long to answer (>#{@timeout || 1}s)."
53
+ end
54
+
55
+ def close
56
+ @service.close
57
+ end
58
+ private
59
+ def with_timeout
60
+ if @timeout
61
+ begin
62
+ timeout(@timeout) do
63
+ yield
64
+ end
65
+ rescue Timeout::Error
66
+ # The timeout might have occurred at any place at all. This means
67
+ # that the connection is probably in an invalid state at this point.
68
+ reconnect
69
+ raise Zack::ServiceTimeout,
70
+ "The server took longer than #{@timeout} seconds to respond."
71
+ end
72
+ else
73
+ yield
74
+ end
75
+ end
76
+
77
+ def reconnect
78
+ @service.close
79
+ connect
80
+ end
81
+ def connect
82
+ @outgoing = Cod.beanstalk(@tube_name, @server)
83
+
84
+ unless @with_answer.empty?
85
+ @incoming = Cod.beanstalk(
86
+ UniqueName.new(@tube_name),
87
+ @server)
88
+ end
89
+
90
+ @service = @outgoing.client(@incoming)
91
+ end
41
92
  end
42
93
  end
data/lib/zack/server.rb CHANGED
@@ -2,22 +2,108 @@
2
2
  module Zack
3
3
  # Server side for RPC calls.
4
4
  #
5
- class Server < Target
5
+ class Server
6
6
  attr_reader :service
7
-
7
+ attr_reader :factory
8
+ attr_reader :server
9
+
10
+ # Initializes a zack server. To specify which class should be the target
11
+ # of the RPC call, you must either give the :factory or the :simple
12
+ # argument.
13
+ #
14
+ # :simple expects a class. This class will be constructed each time a
15
+ # request is made. Then the method will be called on the class.
16
+ #
17
+ # :factory expects a callable (a block or something that has #call) and is
18
+ # passed the control object for the request (see Cod for an explanation of
19
+ # this). You can chose to ignore the control and just use the block to
20
+ # produce an object that is linked to the rest of your program. Or you can
21
+ # link to the rest of the program and the control at the same time.
22
+ #
23
+ # Note that in any case, one object instance _per call_ is created. This
24
+ # is to discourage creating stateful servers. If you still want to do
25
+ # that, well you will just have to code around the limitation, now won't
26
+ # you.
27
+ #
8
28
  def initialize(tube_name, opts={})
9
- super
10
-
11
- channel = Cod.beanstalk(server, tube_name)
12
- @service = Cod::Service.new(channel)
13
- end
14
-
29
+ @server = opts[:server]
30
+
31
+ if opts.has_key? :factory
32
+ @factory = opts[:factory]
33
+ elsif opts.has_key? :simple
34
+ klass = opts[:simple]
35
+ @factory = lambda { |ctl| klass.new }
36
+ else
37
+ raise ArgumentError, "Either :factory or :simple argument must be given."
38
+ end
39
+
40
+ channel = Cod.beanstalk(tube_name, server)
41
+ @service = channel.service
42
+ end
43
+
15
44
  # Handles exactly one request.
16
45
  #
17
- def handle_request
18
- service.one { |(sym, args)|
19
- process_request(sym, args)
46
+ def handle_request(exception_handler=nil)
47
+ service.one { |(sym, args), control|
48
+ exception_handling(exception_handler, control) do
49
+ # p [sym, args]
50
+ process_request(control, sym, args)
51
+ end
20
52
  }
21
53
  end
54
+
55
+ # Processes exactly one request, but doesn't define how the request gets
56
+ # here.
57
+ #
58
+ def process_request(control, sym, args)
59
+ instance = factory.call(control)
60
+
61
+ instance.send(sym, *args)
62
+ end
63
+
64
+ # Runs the server and keeps running until the world ends (or the process,
65
+ # whichever comes first). If you pass a non-nil messages argument, the
66
+ # server will process that many messages and then quit. (Maybe you will
67
+ # want to respawn the server from time to time?)
68
+ #
69
+ # Any exception that is raised inside the RPC code will be passed to the
70
+ # exception_handler block:
71
+ #
72
+ # server.run do |exception, control|
73
+ # # control is the service control object from cod. You can exercise
74
+ # # fine grained message control using this.
75
+ # log.fatal exception
76
+ # end
77
+ #
78
+ # If you don't reraise exceptions from the exception handler block, they
79
+ # will be caught and the server will stay running.
80
+ #
81
+ def run(messages=nil, &exception_handler)
82
+ loop do
83
+ handle_request(exception_handler)
84
+
85
+ if messages
86
+ messages -= 1
87
+ break if messages <= 0
88
+ end
89
+ end
90
+ end
91
+
92
+ private
93
+ # Defines how the server handles exception.
94
+ #
95
+ def exception_handling(exception_handler, control)
96
+ begin
97
+ yield
98
+ rescue => exception
99
+ # If we have an exception handler, it gets handed all the exceptions.
100
+ # No exceptions stop the operation.
101
+ if exception_handler
102
+ exception_handler.call(exception, control)
103
+ else
104
+ raise
105
+ end
106
+ end
107
+ end
22
108
  end
23
109
  end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Exception handling" do
4
+ # Clear old messages (test isolation)
5
+ before(:each) {
6
+ conn = Beanstalk::Connection.new(BEANSTALK_CONNECTION, 'exceptions')
7
+ while conn.peek_ready
8
+ conn.reserve.delete
9
+ end
10
+ conn.close
11
+ }
12
+
13
+ SimpleService = Struct.new(:control, :retry_log) do
14
+ # Used for the exception block specs
15
+ def do_something
16
+ fail
17
+ end
18
+
19
+ # Used for the handler specs
20
+ def retry_message
21
+ retries = (retry_log[control.msg_id] += 1)
22
+ if retries < 2
23
+ control.retry
24
+ end
25
+ end
26
+ end
27
+
28
+ let!(:retry_log) { Hash.new(0) }
29
+
30
+ let(:client) { Zack::Client.new('exceptions',
31
+ server: BEANSTALK_CONNECTION) }
32
+ let(:server) { Zack::Server.new('exceptions',
33
+ server: BEANSTALK_CONNECTION,
34
+ factory: proc { |c| SimpleService.new(c, retry_log) }) }
35
+
36
+ describe 'block given to #run' do
37
+ it "can retry the message" do
38
+ client.do_something
39
+
40
+ retried = false
41
+ server.run(2) do |exception, control|
42
+ retried ? control.delete : control.retry
43
+ retried = true
44
+ end
45
+
46
+ retried.should == true
47
+ end
48
+ it "can use msg_id to retry n times" do
49
+ client.do_something
50
+ client.do_something
51
+
52
+ retry_per_msg = Hash.new(0)
53
+
54
+ server.run(4) do |exception, control|
55
+ retries = (retry_per_msg[control.msg_id] += 1)
56
+ retries < 2 ? control.retry : control.delete
57
+ end
58
+
59
+ retry_per_msg.should have(2).messages
60
+ retry_per_msg.each do |msg_id, retries|
61
+ msg_id.should >0
62
+ retries.should == 2
63
+ end
64
+ end
65
+ end
66
+ describe 'using the control parameter to the factory' do
67
+ it "can retry the message from within the handler" do
68
+ client.retry_message
69
+ server.run(2)
70
+
71
+ retry_log.should have(1).message
72
+
73
+ msg_id, retries = retry_log.first
74
+ msg_id.should > 0
75
+ retries.should == 2
76
+ end
77
+ end
78
+ end
@@ -32,7 +32,8 @@ describe "Regression: " do
32
32
  'regression',
33
33
  :timeout => timeout,
34
34
  :server => BEANSTALK_CONNECTION,
35
- :with_answer => with_answer)
35
+ :with_answer => with_answer).tap { |client|
36
+ self.class.after(:each) { client.close } }
36
37
  end
37
38
 
38
39
  def print_stats
@@ -50,7 +51,7 @@ describe "Regression: " do
50
51
  connection.close
51
52
  end
52
53
 
53
- describe "asynchronous long running message, followed by a short running reader message (bug)" do
54
+ describe "long running message, followed by a short running one (bug)" do
54
55
  class Regression1Server
55
56
  def reader; 42; end
56
57
  def long_running; sleep 0.1 end
@@ -98,11 +99,11 @@ describe "Regression: " do
98
99
  end
99
100
 
100
101
  it "should timeout a blocking call" do
101
- lambda {
102
- lambda {
102
+ expect {
103
+ expect {
103
104
  client.crash_and_burn
104
- }.should_not take_long
105
- }.should raise_error(Zack::ServiceTimeout)
105
+ }.not_to take_long
106
+ }.to raise_error(Zack::ServiceTimeout)
106
107
  end
107
108
  end
108
109
  describe "server that takes a long time, timeout in client stops the operation" do
@@ -119,7 +120,7 @@ describe "Regression: " do
119
120
  it "should pass a sanity check" do
120
121
  client.long_running(0, 42).should == 42
121
122
  end
122
- context "when the first call takes longer than the client timeout" do
123
+ context "message ordering" do
123
124
  before(:each) {
124
125
  begin
125
126
  client.long_running(1.1, 10)
@@ -127,7 +128,7 @@ describe "Regression: " do
127
128
  end
128
129
  }
129
130
 
130
- it "should correctly handle subsequent messages" do
131
+ it "should be preserved" do
131
132
  client.long_running(0, 42).should == 42
132
133
  end
133
134
  end
@@ -10,6 +10,17 @@ describe "Server#run" do
10
10
  end
11
11
  end
12
12
 
13
+ # Isolates the specs by clearing the 'server_run' tube before use.
14
+ before(:each) {
15
+ conn = Beanstalk::Connection.new(BEANSTALK_CONNECTION, 'server_run')
16
+
17
+ while conn.peek_ready
18
+ conn.reserve.delete
19
+ end
20
+
21
+ conn.close
22
+ }
23
+
13
24
  let(:server) { Zack::Server.new(
14
25
  'server_run',
15
26
  :simple => RunServerRun,
@@ -10,6 +10,7 @@ describe Zack::Server do
10
10
  end
11
11
  context "instance" do
12
12
  let(:implementation) { flexmock(:implementation) }
13
+ let(:control) { flexmock(:control) }
13
14
 
14
15
  # Replacing the Cod::Service with a mock
15
16
  let(:service) { flexmock(:service) }
@@ -18,7 +19,7 @@ describe Zack::Server do
18
19
  context "with a factory" do
19
20
  # A small factory that always returns instance.
20
21
  class ImplFactory < Struct.new(:instance)
21
- def call
22
+ def call(control)
22
23
  instance
23
24
  end
24
25
  end
@@ -37,7 +38,7 @@ describe Zack::Server do
37
38
 
38
39
  it "should call the right message on implementation" do
39
40
  service.should_receive(:one).
40
- and_yield([:foobar, [123, '123', :a123]])
41
+ yields([:foobar, [123, '123', :a123]], control)
41
42
 
42
43
  implementation.
43
44
  should_receive(:foobar).
@@ -64,7 +65,7 @@ describe Zack::Server do
64
65
 
65
66
  it "should call the right message on implementation" do
66
67
  service.should_receive(:one).
67
- and_yield([:foobar, [123, '123', :a123]])
68
+ yields([:foobar, [123, '123', :a123]], control)
68
69
 
69
70
  implementation.
70
71
  should_receive(:foobar).
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ require 'beanstalk-client'
3
+
2
4
  require 'zack'
3
5
 
4
6
  RSpec.configure do |config|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,42 +10,41 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2011-08-31 00:00:00.000000000 +02:00
14
- default_executable:
13
+ date: 2011-11-29 00:00:00.000000000Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: cod
18
- requirement: &2165838080 !ruby/object:Gem::Requirement
17
+ requirement: &70152213379200 !ruby/object:Gem::Requirement
19
18
  none: false
20
19
  requirements:
21
20
  - - ~>
22
21
  - !ruby/object:Gem::Version
23
- version: '0.3'
22
+ version: '0.4'
24
23
  type: :runtime
25
24
  prerelease: false
26
- version_requirements: *2165838080
25
+ version_requirements: *70152213379200
27
26
  - !ruby/object:Gem::Dependency
28
- name: beanstalk-client
29
- requirement: &2165837600 !ruby/object:Gem::Requirement
27
+ name: uuid
28
+ requirement: &70152213378720 !ruby/object:Gem::Requirement
30
29
  none: false
31
30
  requirements:
32
31
  - - ~>
33
32
  - !ruby/object:Gem::Version
34
- version: '1.0'
33
+ version: '2.3'
35
34
  type: :runtime
36
35
  prerelease: false
37
- version_requirements: *2165837600
36
+ version_requirements: *70152213378720
38
37
  - !ruby/object:Gem::Dependency
39
- name: uuid
40
- requirement: &2165837140 !ruby/object:Gem::Requirement
38
+ name: beanstalk-client
39
+ requirement: &70152213378240 !ruby/object:Gem::Requirement
41
40
  none: false
42
41
  requirements:
43
42
  - - ~>
44
43
  - !ruby/object:Gem::Version
45
- version: '2.3'
46
- type: :runtime
44
+ version: '1.0'
45
+ type: :development
47
46
  prerelease: false
48
- version_requirements: *2165837140
47
+ version_requirements: *70152213378240
49
48
  description:
50
49
  email:
51
50
  - kaspar.schiess@absurd.li
@@ -61,19 +60,16 @@ files:
61
60
  - README
62
61
  - lib/zack/client.rb
63
62
  - lib/zack/server.rb
64
- - lib/zack/target.rb
65
- - lib/zack/transparent_proxy.rb
66
63
  - lib/zack/unique_name.rb
67
64
  - lib/zack.rb
68
- - spec/integration/regression_spec.rb
69
- - spec/integration/server_runner_spec.rb
65
+ - spec/acceptance/exception_handling_spec.rb
66
+ - spec/acceptance/regression_spec.rb
67
+ - spec/acceptance/server_runner_spec.rb
70
68
  - spec/lib/zack/client_spec.rb
71
69
  - spec/lib/zack/server_spec.rb
72
70
  - spec/spec_helper.rb
73
71
  - example/client.rb
74
- - example/pubsub.rb
75
72
  - example/server.rb
76
- has_rdoc: true
77
73
  homepage: http://github.com/kschiess/zack
78
74
  licenses: []
79
75
  post_install_message:
@@ -88,6 +84,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
84
  - - ! '>='
89
85
  - !ruby/object:Gem::Version
90
86
  version: '0'
87
+ segments:
88
+ - 0
89
+ hash: 2009193566488392526
91
90
  required_rubygems_version: !ruby/object:Gem::Requirement
92
91
  none: false
93
92
  requirements:
@@ -96,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
95
  version: '0'
97
96
  requirements: []
98
97
  rubyforge_project:
99
- rubygems_version: 1.6.2
98
+ rubygems_version: 1.8.10
100
99
  signing_key:
101
100
  specification_version: 3
102
101
  summary: Ruby RPC calls via Cod
data/example/pubsub.rb DELETED
@@ -1,35 +0,0 @@
1
-
2
- class Handler
3
- def foo
4
- puts "Foo was called."
5
- end
6
- def bar
7
- Process.pid
8
- end
9
- def shutdown
10
- exit 0
11
- end
12
- end
13
-
14
- source = Zack::Notifier.new(
15
- 'football',
16
- server: 'localhost:11300',
17
- with_answer: [:bar])
18
-
19
- %w(foo bar).each do |filter|
20
- fork do
21
- handler = Zack::Listener.new(
22
- 'football',
23
- simple: Handler,
24
- server: 'localhost:11300')
25
-
26
- handler.run
27
- end
28
- end
29
-
30
- source.foo
31
- p source.bar
32
-
33
- source.shutdown
34
-
35
- Process.waitall
data/lib/zack/target.rb DELETED
@@ -1,68 +0,0 @@
1
-
2
- module Zack
3
- # Abstract base class for everything that is an RPC target. This implements
4
- # some common mechanisms like a run loop, exception handling and argument
5
- # handling.
6
- #
7
- class Target
8
- attr_reader :factory
9
- attr_reader :server
10
-
11
- # Initializes #factory and #server.
12
- #
13
- def initialize(tube_name, opts={})
14
- @server = opts[:server] || 'beanstalk:11300'
15
-
16
- if opts.has_key? :factory
17
- @factory = opts[:factory]
18
- elsif opts.has_key? :simple
19
- klass = opts[:simple]
20
- @factory = lambda { klass.new }
21
- else
22
- raise ArgumentError, "Either :factory or :simple argument must be given."
23
- end
24
- end
25
-
26
- # Processes exactly one request, but doesn't define how the request gets
27
- # here.
28
- #
29
- def process_request(sym, args)
30
- instance = factory.call
31
-
32
- instance.send(sym, *args)
33
- end
34
-
35
- # Handles one request. This is specific to implementors.
36
- #
37
- def handle_request
38
- raise NotImplementedError,
39
- "Abstract base class doesn't implement #handle_request."
40
- end
41
-
42
- # Runs the server and keeps running until the world ends (or the process,
43
- # whichever comes first).
44
- #
45
- def run(&block)
46
- loop do
47
- exception_handling(block) do
48
- handle_request
49
- end
50
- end
51
- end
52
-
53
- private
54
- # Defines how the server handles exception.
55
- #
56
- def exception_handling(exception_handler)
57
- if exception_handler
58
- begin
59
- yield
60
- rescue => exception
61
- exception_handler.call(exception)
62
- end
63
- else
64
- yield
65
- end
66
- end
67
- end
68
- end
@@ -1,22 +0,0 @@
1
-
2
- # A method missing implementation that will use respond_to? to see wether a
3
- # message should be answered. If yes, it delegates the message to service,
4
- # which is supposed to return one of Cods RPC client primitives. Depending on
5
- # the value of has_answer?(symbol), either #call or #notify is used.
6
- #
7
- module Zack::TransparentProxy
8
- def method_missing(sym, *args, &block)
9
- super unless respond_to?(sym)
10
-
11
- raise ArgumentError, "Can't call methods remotely with a block" if block
12
-
13
- if has_answer?(sym)
14
- return service.call([sym, args])
15
- else
16
- service.notify [sym, args]
17
- return nil
18
- end
19
- rescue Cod::Channel::TimeoutError
20
- raise Zack::ServiceTimeout, "No response from server in the allowed time."
21
- end
22
- end