proxymachine 1.1.0 → 1.2.0

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