zack 0.3.2 → 0.3.3

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