proxymachine 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,10 @@
1
+ = 1.2.0 / 2010-02-09
2
+ * New Features
3
+ * Connection Errors and Timeouts
4
+ * Inactivity Timeouts
5
+ * Enhancements
6
+ * Better async retry logic
7
+
1
8
  = 1.1.0 / 2009-11-05
2
9
  * New Features
3
10
  * Add { :remote, :data, :reply } command [github.com/coderrr]
data/README.md CHANGED
@@ -112,6 +112,48 @@ Valid return values
112
112
  `{ :close => true }` - Close the connection.
113
113
  `{ :close => String }` - Close the connection after sending the String.
114
114
 
115
+ Connection Errors and Timeouts
116
+ ------------------------------
117
+
118
+ It's possible to register a custom callback for handling connection
119
+ errors. The callback is passed the remote when a connection is either
120
+ rejected or a connection timeout occurs:
121
+
122
+ proxy do |data|
123
+ if data =~ /your thing/
124
+ { :remote => 'localhost:1234', :connect_timeout => 1.0 }
125
+ else
126
+ { :noop => true }
127
+ end
128
+ end
129
+
130
+ proxy_connect_error do |remote|
131
+ puts "error connecting to #{remote}"
132
+ end
133
+
134
+ You must provide a `:connect_timeout` value in the `proxy` return value
135
+ to enable connection timeouts. The `:connect_timeout` value is a float
136
+ representing the number of seconds to wait before a connection is
137
+ established. Hard connection rejections always trigger the callback, even
138
+ when no `:connect_timeout` is provided.
139
+
140
+ Inactivity Timeouts
141
+ -------------------
142
+
143
+ Inactivity timeouts work like connect timeouts but are triggered after
144
+ the configured amount of time elapses without receiving the first byte
145
+ of data from an already connected server:
146
+
147
+ proxy do |data|
148
+ { :remote => 'localhost:1234', :inactivity_timeout => 10.0 }
149
+ end
150
+
151
+ proxy_inactivity_error do |remote|
152
+ puts "#{remote} did not send any data for 10 seconds"
153
+ end
154
+
155
+ If no `:inactivity_timeout` is provided, the `proxy_inactivity_error`
156
+ callback is never triggered.
115
157
 
116
158
  Contribute
117
159
  ----------
data/VERSION.yml CHANGED
@@ -1,4 +1,5 @@
1
1
  ---
2
2
  :major: 1
3
- :minor: 1
3
+ :minor: 2
4
4
  :patch: 0
5
+ :build:
data/lib/proxymachine.rb CHANGED
@@ -70,12 +70,30 @@ class ProxyMachine
70
70
  end
71
71
  end
72
72
 
73
+ def self.set_connect_error_callback(&block)
74
+ @@connect_error_callback = block
75
+ end
76
+
77
+ def self.connect_error_callback
78
+ @@connect_error_callback
79
+ end
80
+
81
+ def self.set_inactivity_error_callback(&block)
82
+ @@inactivity_error_callback = block
83
+ end
84
+
85
+ def self.inactivity_error_callback
86
+ @@inactivity_error_callback
87
+ end
88
+
73
89
  def self.run(name, host, port)
74
90
  @@totalcounter = 0
75
91
  @@maxcounter = 0
76
92
  @@counter = 0
77
93
  @@name = name
78
94
  @@listen = "#{host}:#{port}"
95
+ @@connect_error_callback ||= proc { |remote| }
96
+ @@inactivity_error_callback ||= proc { |remote| }
79
97
  self.update_procline
80
98
  EM.epoll
81
99
 
@@ -107,4 +125,12 @@ module Kernel
107
125
  def proxy(&block)
108
126
  ProxyMachine.set_router(block)
109
127
  end
110
- end
128
+
129
+ def proxy_connect_error(&block)
130
+ ProxyMachine.set_connect_error_callback(&block)
131
+ end
132
+
133
+ def proxy_inactivity_error(&block)
134
+ ProxyMachine.set_inactivity_error_callback(&block)
135
+ end
136
+ end
@@ -10,7 +10,11 @@ class ProxyMachine
10
10
  def post_init
11
11
  LOGGER.info "Accepted #{peer}"
12
12
  @buffer = []
13
+ @remote = nil
13
14
  @tries = 0
15
+ @connected = false
16
+ @connect_timeout = nil
17
+ @inactivity_timeout = nil
14
18
  ProxyMachine.incr
15
19
  end
16
20
 
@@ -23,73 +27,95 @@ class ProxyMachine
23
27
  end
24
28
 
25
29
  def receive_data(data)
26
- if !@server_side
30
+ if !@connected
27
31
  @buffer << data
28
- ensure_server_side_connection
32
+ establish_remote_server if @remote.nil?
29
33
  end
30
34
  rescue => e
31
35
  close_connection
32
36
  LOGGER.info "#{e.class} - #{e.message}"
33
37
  end
34
38
 
35
- def ensure_server_side_connection
36
- @timer.cancel if @timer
37
- unless @server_side
38
- commands = ProxyMachine.router.call(@buffer.join)
39
- LOGGER.info "#{peer} #{commands.inspect}"
40
- close_connection unless commands.instance_of?(Hash)
41
- if remote = commands[:remote]
42
- m, host, port = *remote.match(/^(.+):(.+)$/)
43
- if try_server_connect(host, port.to_i)
44
- if data = commands[:data]
45
- @buffer = [data]
46
- end
47
- if reply = commands[:reply]
48
- send_data(reply)
49
- end
50
- send_and_clear_buffer
51
- end
52
- elsif close = commands[:close]
53
- if close == true
54
- close_connection
55
- else
56
- send_data(close)
57
- close_connection_after_writing
58
- end
59
- elsif commands[:noop]
60
- # do nothing
61
- else
39
+ # Called when new data is available from the client but no remote
40
+ # server has been established. If a remote can be established, an
41
+ # attempt is made to connect and proxy to the remote server.
42
+ def establish_remote_server
43
+ fail "establish_remote_server called with remote established" if @remote
44
+ commands = ProxyMachine.router.call(@buffer.join)
45
+ LOGGER.info "#{peer} #{commands.inspect}"
46
+ close_connection unless commands.instance_of?(Hash)
47
+ if remote = commands[:remote]
48
+ m, host, port = *remote.match(/^(.+):(.+)$/)
49
+ @remote = [host, port]
50
+ if data = commands[:data]
51
+ @buffer = [data]
52
+ end
53
+ if reply = commands[:reply]
54
+ send_data(reply)
55
+ end
56
+ @connect_timeout = commands[:connect_timeout]
57
+ @inactivity_timeout = commands[:inactivity_timeout]
58
+ connect_to_server
59
+ elsif close = commands[:close]
60
+ if close == true
62
61
  close_connection
62
+ else
63
+ send_data(close)
64
+ close_connection_after_writing
63
65
  end
66
+ elsif commands[:noop]
67
+ # do nothing
68
+ else
69
+ close_connection
64
70
  end
65
71
  end
66
72
 
67
- def try_server_connect(host, port)
73
+ # Connect to the remote server
74
+ def connect_to_server
75
+ fail "connect_server called without remote established" if @remote.nil?
76
+ host, port = @remote
77
+ LOGGER.info "Establishing new connection with #{host}:#{port}"
68
78
  @server_side = ServerConnection.request(host, port, self)
69
- proxy_incoming_to(@server_side, 10240)
70
- LOGGER.info "Successful connection to #{host}:#{port}."
71
- true
72
- rescue => e
79
+ @server_side.pending_connect_timeout = @connect_timeout
80
+ @server_side.comm_inactivity_timeout = @inactivity_timeout
81
+ end
82
+
83
+ # Called by the server side immediately after the server connection was
84
+ # successfully established. Send any buffer we've accumulated and start
85
+ # raw proxying.
86
+ def server_connection_success
87
+ LOGGER.info "Successful connection to #{@remote.join(':')}"
88
+ @connected = true
89
+ @buffer.each { |data| @server_side.send_data(data) }
90
+ proxy_incoming_to @server_side
91
+ end
92
+
93
+ # Called by the server side when a connection could not be established,
94
+ # either due to a hard connection failure or to a connection timeout.
95
+ # Leave the client connection open and retry the server connection up to
96
+ # 10 times.
97
+ def server_connection_failed
98
+ @server_side = nil
73
99
  if @tries < 10
74
100
  @tries += 1
75
- LOGGER.info "Failed on server connect attempt #{@tries}. Trying again..."
76
- @timer.cancel if @timer
77
- @timer = EventMachine::Timer.new(0.1) do
78
- self.ensure_server_side_connection
79
- end
101
+ LOGGER.info "Retrying connection with #{@remote.join(':')} (##{@tries})"
102
+ EM.add_timer(0.1) { connect_to_server }
80
103
  else
81
- LOGGER.info "Failed after ten connection attempts."
104
+ LOGGER.info "Connect #{@remote.join(':')} failed after ten attempts."
105
+ close_connection
106
+ ProxyMachine.connect_error_callback.call(@remote.join(':'))
82
107
  end
83
- false
84
108
  end
85
109
 
86
- def send_and_clear_buffer
87
- if !@buffer.empty?
88
- @buffer.each do |x|
89
- @server_side.send_data(x)
90
- end
91
- @buffer = []
92
- end
110
+ # Called by the server when an inactivity timeout is detected. The timeout
111
+ # argument is the configured inactivity timeout in seconds as a float; the
112
+ # elapsed argument is the amount of time that actually elapsed since
113
+ # connecting but not receiving any data.
114
+ def server_inactivity_timeout(timeout, elapsed)
115
+ LOGGER.info "Disconnecting #{@remote.join(':')} after #{elapsed}s of inactivity (> #{timeout.inspect})"
116
+ @server_side = nil
117
+ close_connection
118
+ ProxyMachine.inactivity_error_callback.call(@remote.join(':'))
93
119
  end
94
120
 
95
121
  def unbind
@@ -6,14 +6,40 @@ class ProxyMachine
6
6
 
7
7
  def initialize(conn)
8
8
  @client_side = conn
9
+ @connected = false
10
+ @data_received = false
11
+ @timeout = nil
9
12
  end
10
13
 
11
- def post_init
12
- proxy_incoming_to(@client_side, 10240)
14
+ def receive_data(data)
15
+ fail "receive_data called after raw proxy enabled" if @data_received
16
+ @data_received = true
17
+ @client_side.send_data(data)
18
+ proxy_incoming_to @client_side
19
+ end
20
+
21
+ def connection_completed
22
+ @connected = Time.now
23
+ @timeout = comm_inactivity_timeout || 0.0
24
+ @client_side.server_connection_success
13
25
  end
14
26
 
15
27
  def unbind
16
- @client_side.close_connection_after_writing
28
+ now = Time.now
29
+ if !@connected
30
+ @client_side.server_connection_failed
31
+ elsif !@data_received
32
+ if @timeout > 0.0 && (elapsed = now - @connected) >= @timeout
33
+ # EM aborted the connection due to an inactivity timeout
34
+ @client_side.server_inactivity_timeout(@timeout, elapsed)
35
+ else
36
+ # server disconnected soon after connecting without sending data
37
+ # treat this like a failed server connection
38
+ @client_side.server_connection_failed
39
+ end
40
+ else
41
+ @client_side.close_connection_after_writing
42
+ end
17
43
  end
18
44
  end
19
45
  end
data/proxymachine.gemspec CHANGED
@@ -1,15 +1,15 @@
1
1
  # Generated by jeweler
2
- # DO NOT EDIT THIS FILE
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
4
4
  # -*- encoding: utf-8 -*-
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{proxymachine}
8
- s.version = "1.1.0"
8
+ s.version = "1.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Tom Preston-Werner"]
12
- s.date = %q{2009-11-05}
12
+ s.date = %q{2010-02-09}
13
13
  s.default_executable = %q{proxymachine}
14
14
  s.email = %q{tom@mojombo.com}
15
15
  s.executables = ["proxymachine"]
@@ -64,3 +64,4 @@ Gem::Specification.new do |s|
64
64
  s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
65
65
  end
66
66
  end
67
+
@@ -15,7 +15,21 @@ proxy do |data|
15
15
  { :remote => "localhost:9980" }
16
16
  elsif data == 'g'
17
17
  { :remote => "localhost:9980", :data => 'g2', :reply => 'g3-' }
18
+ elsif data == 'connect reject'
19
+ { :remote => "localhost:9989" }
20
+ elsif data == 'inactivity'
21
+ { :remote => "localhost:9980", :data => 'sleep 3', :inactivity_timeout => 1 }
18
22
  else
19
23
  { :close => true }
20
24
  end
21
- end
25
+ end
26
+
27
+ ERROR_FILE = File.expand_path('../../proxy_error', __FILE__)
28
+
29
+ proxy_connect_error do |remote|
30
+ File.open(ERROR_FILE, 'wb') { |fd| fd.write("connect error: #{remote}") }
31
+ end
32
+
33
+ proxy_inactivity_error do |remote|
34
+ File.open(ERROR_FILE, 'wb') { |fd| fd.write("activity error: #{remote}") }
35
+ end
@@ -8,6 +8,14 @@ def assert_proxy(host, port, send, recv)
8
8
  end
9
9
 
10
10
  class ProxymachineTest < Test::Unit::TestCase
11
+ def setup
12
+ @proxy_error_file = "#{File.dirname(__FILE__)}/proxy_error"
13
+ end
14
+
15
+ def teardown
16
+ File.unlink(@proxy_error_file) rescue nil
17
+ end
18
+
11
19
  should "handle simple routing" do
12
20
  assert_proxy('localhost', 9990, 'a', '9980:a')
13
21
  assert_proxy('localhost', 9990, 'b', '9981:b')
@@ -40,4 +48,24 @@ class ProxymachineTest < Test::Unit::TestCase
40
48
  assert_equal '9980:' + 'e' * 2048 + 'f', sock.read
41
49
  sock.close
42
50
  end
51
+
52
+ should "call proxy_connect_error when a connection is rejected" do
53
+ sock = TCPSocket.new('localhost', 9990)
54
+ sock.write('connect reject')
55
+ sock.flush
56
+ assert_equal "", sock.read
57
+ sock.close
58
+ assert_equal "connect error: localhost:9989", File.read(@proxy_error_file)
59
+ end
60
+
61
+ should "call proxy_inactivity_error when initial read times out" do
62
+ sock = TCPSocket.new('localhost', 9990)
63
+ sent = Time.now
64
+ sock.write('inactivity')
65
+ sock.flush
66
+ assert_equal "", sock.read
67
+ assert_operator Time.now - sent, :>=, 1.0
68
+ assert_equal "activity error: localhost:9980", File.read(@proxy_error_file)
69
+ sock.close
70
+ end
43
71
  end
data/test/test_helper.rb CHANGED
@@ -17,6 +17,7 @@ module EventMachine
17
17
  end
18
18
 
19
19
  def receive_data(data)
20
+ sleep $1.to_f if data =~ /^sleep (.*)/
20
21
  send_data("#{@@port}:#{data}")
21
22
  close_connection_after_writing
22
23
  end
@@ -54,4 +55,4 @@ end
54
55
  EventMachine::Protocols::TestConnection.start('localhost', port)
55
56
  end
56
57
  end
57
- end
58
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proxymachine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Preston-Werner
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-05 00:00:00 -08:00
12
+ date: 2010-02-09 00:00:00 -08:00
13
13
  default_executable: proxymachine
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency