resilient_socket 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2012 Clarity Services, Inc.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ resilient_socket
2
+ ================
3
+
4
+ A Resilient TCP Socket Client with built-in timeouts, retries, and logging
5
+
6
+ * http://github.com/ClarityServices/resilient_socket
7
+
8
+ ### Introduction
9
+
10
+ Resilient Socket implements resilience features that most developers wish was
11
+ already included in the standard Ruby libraries.
12
+
13
+ With so many "client" libraries to servers such us memcache, MongoDB, Redis, etc.
14
+ their focus on the communication formats and messaging interactions. As a result
15
+ adding resilience is usually an after thought.
16
+
17
+ More importantly the way that each client implements connection failure handling
18
+ varies dramatically. The purpose of this library is to try and extract the best
19
+ of all the socket error handling out there and create a consistent way of dealing
20
+ with connection failures.
21
+
22
+ Another important feature is that the _connect_ and _read_ API's use timeout's to
23
+ prevent a network issue from "hanging" the client program.
24
+
25
+ It is expected that this library will undergo significant changes until V1 is reached
26
+ as input is gathered from client library developers. After V1 the interface should
27
+ not break existing users
28
+
29
+ ### TCPClient API
30
+
31
+ #### Standard Logging methods
32
+
33
+ TCPClient should be a drop in replacement for TCPSocket when used as a client
34
+ in any way needed other than for the initializer that accepts several new options
35
+ to adjust the retry logic
36
+
37
+ ### Dependencies
38
+
39
+ - Ruby MRI 1.8.7 (or above) Or, JRuby 1.6.3 (or above)
40
+ - SemanticLogger
41
+
42
+ ### Install
43
+
44
+ gem install semantic-logger
45
+
46
+ To log to MongoDB
47
+
48
+ gem install mongo
49
+
50
+ ### Future
51
+
52
+ - Web Interface to view and search log information stored in MongoDB
53
+
54
+ Development
55
+ -----------
56
+
57
+ Want to contribute to Semantic Logger?
58
+
59
+ First clone the repo and run the tests:
60
+
61
+ git clone git://github.com/ClarityServices/semantic-logger.git
62
+ cd semantic-logger
63
+ jruby -S rake test
64
+
65
+ Feel free to ping the mailing list with any issues and we'll try to resolve it.
66
+
67
+ Contributing
68
+ ------------
69
+
70
+ Once you've made your great commits:
71
+
72
+ 1. [Fork](http://help.github.com/forking/) semantic-logger
73
+ 2. Create a topic branch - `git checkout -b my_branch`
74
+ 3. Push to your branch - `git push origin my_branch`
75
+ 4. Create an [Issue](http://github.com/ClarityServices/semantic-logger/issues) with a link to your branch
76
+ 5. That's it!
77
+
78
+ Meta
79
+ ----
80
+
81
+ * Code: `git clone git://github.com/ClarityServices/semantic-logger.git`
82
+ * Home: <https://github.com/ClarityServices/semantic-logger>
83
+ * Docs: TODO <http://ClarityServices.github.com/semantic-logger/>
84
+ * Bugs: <http://github.com/reidmorrison/semantic-logger/issues>
85
+ * Gems: <http://rubygems.org/gems/semantic-logger>
86
+
87
+ This project uses [Semantic Versioning](http://semver.org/).
88
+
89
+ Authors
90
+ -------
91
+
92
+ Reid Morrison :: reidmo@gmail.com :: @reidmorrison
93
+
94
+ License
95
+ -------
96
+
97
+ Copyright 2012 Clarity Services, Inc.
98
+
99
+ Licensed under the Apache License, Version 2.0 (the "License");
100
+ you may not use this file except in compliance with the License.
101
+ You may obtain a copy of the License at
102
+
103
+ http://www.apache.org/licenses/LICENSE-2.0
104
+
105
+ Unless required by applicable law or agreed to in writing, software
106
+ distributed under the License is distributed on an "AS IS" BASIS,
107
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
108
+ See the License for the specific language governing permissions and
109
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'rubygems'
5
+ require 'rake/clean'
6
+ require 'rake/testtask'
7
+ require 'date'
8
+ require 'resilient_socket/version'
9
+
10
+ desc "Build gem"
11
+ task :gem do |t|
12
+ gemspec = Gem::Specification.new do |spec|
13
+ spec.name = 'resilient_socket'
14
+ spec.version = ResilientSocket::VERSION
15
+ spec.platform = Gem::Platform::RUBY
16
+ spec.authors = ['Reid Morrison']
17
+ spec.email = ['reidmo@gmail.com']
18
+ spec.homepage = 'https://github.com/ClarityServices/resilient_socket'
19
+ spec.date = Date.today.to_s
20
+ spec.summary = "A Resilient TCP Socket Client with built-in timeouts, retries, and logging"
21
+ spec.description = "A Resilient TCP Socket Client with built-in timeouts, retries, and logging"
22
+ spec.files = FileList["./**/*"].exclude('*.gem', 'nbproject').map{|f| f.sub(/^\.\//, '')}
23
+ spec.has_rdoc = true
24
+ spec.add_dependency 'semantic_logger'
25
+ end
26
+ Gem::Builder.new(gemspec).build
27
+ end
28
+
29
+ desc "Run Test Suite"
30
+ task :test do
31
+ Rake::TestTask.new(:functional) do |t|
32
+ t.test_files = FileList['test/*_test.rb']
33
+ t.verbose = true
34
+ end
35
+
36
+ Rake::Task['functional'].invoke
37
+ end
@@ -0,0 +1,8 @@
1
+ module ResilientSocket
2
+
3
+ class Exception < ::RuntimeError; end
4
+ class ConnectionTimeout < Exception; end
5
+ class ReadTimeout < Exception; end
6
+ class ConnectionFailure < Exception; end
7
+
8
+ end
@@ -0,0 +1,351 @@
1
+ module ResilientSocket
2
+
3
+ # Make Socket calls resilient by adding timeouts, retries and specific
4
+ # exception categories
5
+ #
6
+ # Resilient TCP Client with:
7
+ # * Connection Timeouts
8
+ # Ability to timeout if a connect does not complete within a reasonable time
9
+ # For example, this can occur when the server is turned off without shutting down
10
+ # causing clients to hang creating new connections
11
+ #
12
+ # * Automatic retries on startup connection failure
13
+ # For example, the server is being restarted while the client is starting
14
+ # Gives the server a few seconds to restart to
15
+ #
16
+ # * Automatic retries on active connection failures
17
+ # If the server is restarted during
18
+ #
19
+ # Connection and Read Timeouts are fully configurable
20
+ #
21
+ # Raises ConnectionTimeout when the connection timeout is exceeded
22
+ # Raises ReadTimeout when the read timeout is exceeded
23
+ # Raises ConnectionFailure when a network error occurs whilst reading or writing
24
+ #
25
+ # Future:
26
+ #
27
+ # * Automatic failover to another server should the current server not respond
28
+ # to a connection request by supplying an array of host names
29
+ #
30
+ class TCPClient
31
+ # Supports embedding user supplied data along with this connection
32
+ # such as sequence number, etc.
33
+ # TCPClient will reset this value to nil on connection start and
34
+ # after a connection is re-established. For example on automatic reconnect
35
+ # due to a failed connection to the server
36
+ attr_accessor :user_data
37
+
38
+ # [String] Name of the server currently connected to or being connected to
39
+ # including the port number
40
+ #
41
+ # Example:
42
+ # "localhost:2000"
43
+ attr_reader :server
44
+
45
+ # Create a connection, call the supplied block and close the connection on
46
+ # completion of the block
47
+ #
48
+ # See #initialize for the list of parameters
49
+ #
50
+ # Example
51
+ # ResilientSocket::TCPClient.connect(
52
+ # :server => 'server:3300',
53
+ # :connect_retry_interval => 0.1,
54
+ # :connect_retry_count => 5
55
+ # ) do |client|
56
+ # client.retry_on_connection_failure do
57
+ # client.send('Update the database')
58
+ # end
59
+ # response = client.read(20)
60
+ # puts "Received: #{response}"
61
+ # end
62
+ #
63
+ def self.connect(params={})
64
+ begin
65
+ connection = self.new(params)
66
+ yield(connection)
67
+ ensure
68
+ connection.close if connection
69
+ end
70
+ end
71
+
72
+ # Create a new TCP Client connection
73
+ #
74
+ # Parameters:
75
+ # :server [String]
76
+ # URL of the server to connect to with port number
77
+ # 'localhost:2000'
78
+ #
79
+ # :servers [Array of String]
80
+ # Array of URL's of servers to connect to with port numbers
81
+ # ['server1:2000', 'server2:2000']
82
+ #
83
+ # The second server will only be attempted once the first server
84
+ # cannot be connected to or has timed out on connect
85
+ # A read failure or timeout will not result in switching to the second
86
+ # server, only a connection failure or during an automatic reconnect
87
+ #
88
+ # :read_timeout [Float]
89
+ # Time in seconds to timeout on read
90
+ # Can be overridden by supplying a timeout in the read call
91
+ # Default: 60
92
+ #
93
+ # :connect_timeout [Float]
94
+ # Time in seconds to timeout when trying to connect to the server
95
+ # Default: Half of the :read_timeout ( 30 seconds )
96
+ #
97
+ # :log_level [Symbol]
98
+ # Only set this level to override the global SemanticLogger logging level
99
+ # Can be used to turn on trace or debug level logging in production
100
+ # Any valid SemanticLogger log level:
101
+ # :trace, :debug, :info, :warn, :error, :fatal
102
+ #
103
+ # :buffered [Boolean]
104
+ # Whether to use Nagle's Buffering algorithm (http://en.wikipedia.org/wiki/Nagle's_algorithm)
105
+ # Recommend disabling for RPC style invocations where we don't want to wait for an
106
+ # ACK from the server before sending the last partial segment
107
+ # Buffering is recommended in a browser or file transfer style environment
108
+ # where multiple sends are expected during a single response
109
+ # Default: true
110
+ #
111
+ # :connect_retry_count [Fixnum]
112
+ # Number of times to retry connecting when a connection fails
113
+ # Default: 10
114
+ #
115
+ # :connect_retry_interval [Float]
116
+ # Number of seconds between connection retry attempts after the first failed attempt
117
+ # Default: 0.5
118
+ #
119
+ # Example
120
+ # client = ResilientSocket::TCPClient.new(
121
+ # :server => 'server:3300',
122
+ # :connect_retry_interval => 0.1,
123
+ # :connect_retry_count => 5
124
+ # )
125
+ #
126
+ # client.retry_on_connection_failure do
127
+ # client.send('Update the database')
128
+ # end
129
+ #
130
+ # response = client.read(20)
131
+ # puts "Received: #{response}"
132
+ # client.close
133
+ def initialize(parameters={})
134
+ params = parameters.dup
135
+ @read_timeout = (params.delete(:read_timeout) || 60.0).to_f
136
+ @connect_timeout = (params.delete(:connect_timeout) || (@read_timeout/2)).to_f
137
+ buffered = params.delete(:buffered)
138
+ @buffered = buffered.nil? ? true : buffered
139
+ @connect_retry_count = params.delete(:connect_retry_count) || 10
140
+ @connect_retry_interval = (params.delete(:connect_retry_interval) || 0.5).to_f
141
+
142
+ unless @servers = params[:servers]
143
+ raise "Missing mandatory :server or :servers" unless server = params.delete(:server)
144
+ @servers = [ server ]
145
+ end
146
+ @logger = SemanticLogger::Logger.new("#{self.class.name} #{@servers.inspect}", params[:log_level] || SemanticLogger::Logger.default_level)
147
+ params.each_pair {|k,v| @logger.warn "Ignoring unknown option #{k} = #{v}"}
148
+
149
+ # Connect to the Server
150
+ connect
151
+ end
152
+
153
+ # Connect to the TCP server
154
+ #
155
+ # Raises ConnectionTimeout when the time taken to create a connection
156
+ # exceeds the :connect_timeout
157
+ # Raises ConnectionFailure whenever Socket raises an error such as Error::EACCESS etc, see Socket#connect for more information
158
+ #
159
+ # Error handling is implemented as follows:
160
+ # 1. TCP Socket Connect failure:
161
+ # Cannot reach server
162
+ # Server is being restarted, or is not running
163
+ # Retry 50 times every 100ms before raising a ConnectionFailure
164
+ # - Means all calls to #connect will take at least 5 seconds before failing if the server is not running
165
+ # - Allows hot restart of server process if it restarts within 5 seconds
166
+ #
167
+ # 2. TCP Socket Connect timeout:
168
+ # Timed out after 5 seconds trying to connect to the server
169
+ # Usually means server is busy or the remote server disappeared off the network recently
170
+ # No retry, just raise a ConnectionTimeout
171
+ def connect
172
+ retries = 0
173
+ @logger.benchmark_info "Connected to server" do
174
+ begin
175
+ # TODO Implement failover to second server if connect fails
176
+ @server = @servers.first
177
+
178
+ host_name, port = @server.split(":")
179
+ port = port.to_i
180
+ address = Socket.getaddrinfo('localhost', nil, Socket::AF_INET)
181
+
182
+ socket = Socket.new(Socket.const_get(address[0][0]), Socket::SOCK_STREAM, 0)
183
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) unless @buffered
184
+
185
+ # http://stackoverflow.com/questions/231647/how-do-i-set-the-socket-timeout-in-ruby
186
+ begin
187
+ socket_address = Socket.pack_sockaddr_in(port, address[0][3])
188
+ socket.connect_nonblock(socket_address)
189
+ rescue Errno::EINPROGRESS
190
+ resp = IO.select(nil, [socket], nil, @connect_timeout)
191
+ raise(ConnectionTimeout.new("Timedout after #{@connect_timeout} seconds trying to connect to #{host_name}:#{port}")) unless resp
192
+ begin
193
+ socket_address = Socket.pack_sockaddr_in(port, address[0][3])
194
+ socket.connect_nonblock(socket_address)
195
+ rescue Errno::EISCONN
196
+ end
197
+ end
198
+ @socket = socket
199
+
200
+ rescue SystemCallError => exception
201
+ if retries < @connect_retry_count
202
+ retries += 1
203
+ @logger.warn "Connection failure: #{exception.class}: #{exception.message}. Retry: #{retries}"
204
+ sleep @connect_retry_interval
205
+ retry
206
+ end
207
+ @logger.error "Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
208
+ raise ConnectionFailure.new("After #{retries} attempts: #{exception.class}: #{exception.message}")
209
+ end
210
+ end
211
+ self.user_data = nil
212
+ true
213
+ end
214
+
215
+ # Send data to the server
216
+ #
217
+ # Use #with_retry to add resilience to the #send method
218
+ #
219
+ # Raises ConnectionFailure whenever the send fails
220
+ # For a description of the errors, see Socket#write
221
+ #
222
+ def send(data)
223
+ @logger.trace("==> Sending", data)
224
+ @logger.benchmark_debug("==> #send Sent #{data.length} bytes") do
225
+ begin
226
+ @socket.write(data)
227
+ rescue SystemCallError => exception
228
+ @logger.warn "#send Connection failure: #{exception.class}: #{exception.message}"
229
+ close
230
+ raise ConnectionFailure.new("Send Connection failure: #{exception.class}: #{exception.message}")
231
+ end
232
+ end
233
+ end
234
+
235
+ # Send data with retry logic
236
+ #
237
+ # On a connection failure, it will close the connection and retry the block
238
+ # Returns immediately on exception ReadTimeout
239
+ #
240
+ # Note this method should only wrap a single standalone call to the server
241
+ # since it will automatically retry the entire block every time a
242
+ # connection failure is experienced
243
+ #
244
+ # Error handling is implemented as follows:
245
+ # Network failure during send of either the header or the body
246
+ # Since the entire message was not sent it is assumed that it will not be processed
247
+ # Close socket
248
+ # Retry 1 time using a new connection before raising a ConnectionFailure
249
+ #
250
+ # Example of a resilient request that could _modify_ data at the server:
251
+ #
252
+ # # Only the send is within the retry block since we cannot re-send once
253
+ # # the send was successful
254
+ #
255
+ # Example of a resilient _read-only_ request:
256
+ #
257
+ # # Since the send can be sent many times it is safe to also put the receive
258
+ # # inside the retry block
259
+ #
260
+ def retry_on_connection_failure
261
+ retries = 0
262
+ begin
263
+ connect if closed?
264
+ yield(self)
265
+ rescue ConnectionFailure => exception
266
+ close
267
+ if retries < 3
268
+ retries += 1
269
+ @logger.warn "#retry_on_connection_failure Connection failure: #{exception.message}. Retry: #{retries}"
270
+ connect
271
+ retry
272
+ end
273
+ @logger.error "#retry_on_connection_failure Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
274
+ raise ConnectionFailure.new("After #{retries} retry_on_connection_failure attempts: #{exception.class}: #{exception.message}")
275
+ rescue Exception => exc
276
+ # With any other exception we have to close the connection since the connection
277
+ # is now in an unknown state
278
+ close
279
+ raise exc
280
+ end
281
+ end
282
+
283
+ # 4. TCP receive timeout:
284
+ # Send was successful but receive timed out after X seconds (for example 10 seconds)
285
+ # No data or partial data received ( for example header but no body )
286
+ # Close socket
287
+ # Don't retry since it could result in duplicating the request
288
+ # No retry, just raise a ReadTimeout
289
+ #
290
+ # Parameters
291
+ # maxlen [Fixnum]
292
+ # The Maximum number of bytes to return
293
+ # Very often less than maxlen bytes will be returned
294
+ #
295
+ # timeout [Float]
296
+ # Optional: Override the default read timeout for this read
297
+ # Number of seconds before raising ReadTimeout when no data has
298
+ # been returned
299
+ # Default: :read_timeout supplied to #initialize
300
+ def read(maxlen, buffer=nil, timeout=nil)
301
+ buffer ||= ''
302
+ @logger.benchmark_debug("<== #read Received upto #{maxlen} bytes") do
303
+ # Block on data to read for @read_timeout seconds
304
+ begin
305
+ ready = IO.select([@socket], nil, [@socket], timeout || @read_timeout)
306
+ unless ready
307
+ @logger.warn "#read Timeout waiting for server to reply"
308
+ close
309
+ raise ReadTimeout.new("Timedout after #{timeout || @read_timeout} seconds trying to read from #{@server}")
310
+ end
311
+ rescue IOError => exception
312
+ @logger.warn "#read Connection failure while waiting for data: #{exception.class}: #{exception.message}"
313
+ close
314
+ raise ConnectionFailure, "#{exception.class}: #{exception.message}"
315
+ end
316
+
317
+ # Read data from socket
318
+ begin
319
+ @socket.sysread(maxlen, buffer)
320
+ @logger.trace("<== #read Received", buffer)
321
+ rescue SystemCallError, IOError => exception
322
+ @logger.warn "#read Connection failure while reading data: #{exception.class}: #{exception.message}"
323
+ close
324
+ raise ConnectionFailure, "#{exception.class}: #{exception.message}"
325
+ end
326
+ end
327
+ buffer
328
+ end
329
+
330
+ # Close the socket
331
+ #
332
+ # Logs a warning if an error occurs trying to close the socket
333
+ def close
334
+ @socket.close unless @socket.closed?
335
+ rescue IOError => exception
336
+ @logger.warn "IOError when attempting to close socket: #{exception.class}: #{exception.message}"
337
+ end
338
+
339
+ # Returns whether the socket is closed
340
+ def closed?
341
+ @socket.closed?
342
+ end
343
+
344
+ # See: Socket#setsockopt
345
+ def setsockopt(level, optname, optval)
346
+ @socket.setsockopt(level, optname, optval)
347
+ end
348
+
349
+ end
350
+
351
+ end
@@ -0,0 +1,3 @@
1
+ module ResilientSocket #:nodoc
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'socket'
2
+ require 'semantic_logger'
3
+
4
+ require 'resilient_socket/version'
5
+ require 'resilient_socket/exceptions'
6
+ module ResilientSocket
7
+ autoload :TCPClient, 'resilient_socket/tcp_client'
8
+ end
@@ -0,0 +1,2 @@
1
+ file.reference.resilient_socket-lib=/Users/rmorrison/Sandbox/resilient_socket/lib
2
+ file.reference.resilient_socket-test=/Users/rmorrison/Sandbox/resilient_socket/test
@@ -0,0 +1,4 @@
1
+ clean=Remove any temporary products.
2
+ clobber=Remove any generated file.
3
+ gem=Build gem
4
+ test=Run Test Suite
@@ -0,0 +1,7 @@
1
+ file.reference.resilient_socket-lib=lib
2
+ file.reference.resilient_socket-test=test
3
+ main.file=
4
+ platform.active=Ruby
5
+ source.encoding=UTF-8
6
+ src.dir=${file.reference.resilient_socket-lib}
7
+ test.src.dir=${file.reference.resilient_socket-test}
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xmlns="http://www.netbeans.org/ns/project/1">
3
+ <type>org.netbeans.modules.ruby.rubyproject</type>
4
+ <configuration>
5
+ <data xmlns="http://www.netbeans.org/ns/ruby-project/1">
6
+ <name>resilient_socket</name>
7
+ <source-roots>
8
+ <root id="src.dir"/>
9
+ </source-roots>
10
+ <test-roots>
11
+ <root id="test.src.dir"/>
12
+ </test-roots>
13
+ </data>
14
+ </configuration>
15
+ </project>
Binary file
@@ -0,0 +1,114 @@
1
+ require 'rubygems'
2
+ require 'socket'
3
+ require 'bson'
4
+ require 'semantic_logger'
5
+
6
+ # Read the bson document, returning nil if the IO is closed
7
+ # before receiving any data or a complete BSON document
8
+ def read_bson_document(io)
9
+ bytebuf = BSON::ByteBuffer.new
10
+ # Read 4 byte size of following BSON document
11
+ bytes = io.read(4)
12
+ return unless bytes
13
+ # Read BSON document
14
+ sz = bytes.unpack("V")[0]
15
+ bytebuf.append!(bytes)
16
+ bytes = io.read(sz-4)
17
+ return unless bytes
18
+ bytebuf.append!(bytes)
19
+ return BSON.deserialize(bytebuf)
20
+ end
21
+
22
+ # Simple single threaded server for testing purposes using a local socket
23
+ # Sends and receives BSON Messages
24
+ class SimpleTCPServer
25
+ attr_reader :thread
26
+ def initialize(port = 2000)
27
+ start(port)
28
+ end
29
+
30
+ def start(port)
31
+ @server = TCPServer.open(port)
32
+ @logger = SemanticLogger::Logger.new(self.class)
33
+
34
+ @thread = Thread.new do
35
+ loop do
36
+ @logger.debug "Waiting for a client to connect"
37
+
38
+ # Wait for a client to connect
39
+ on_request(@server.accept)
40
+ end
41
+ end
42
+ end
43
+
44
+ def stop
45
+ if @thread
46
+ @thread.kill
47
+ @thread.join
48
+ @thread = nil
49
+ end
50
+ begin
51
+ @server.close if @server
52
+ rescue IOError
53
+ end
54
+ end
55
+
56
+ # Called for each message received from the client
57
+ # Returns a Hash that is sent back to the caller
58
+ def on_message(message)
59
+ case message['action']
60
+ when 'test1'
61
+ { 'result' => 'test1' }
62
+ when 'sleep'
63
+ sleep message['duration'] || 1
64
+ { 'result' => 'sleep' }
65
+ when 'fail'
66
+ if message['attempt'].to_i >= 2
67
+ { 'result' => 'fail' }
68
+ else
69
+ nil
70
+ end
71
+ else
72
+ { 'result' => "Unknown action: #{message['action']}" }
73
+ end
74
+ end
75
+
76
+ # Called for each client connection
77
+ # In a real server each request would be handled in a separate thread
78
+ def on_request(client)
79
+ @logger.debug "Client connected, waiting for data from client"
80
+
81
+ while(request = read_bson_document(client)) do
82
+ @logger.debug "\n****************** Received request"
83
+ @logger.trace 'Request', request
84
+ break unless request
85
+
86
+ if reply = on_message(request)
87
+ @logger.debug "Sending Reply"
88
+ @logger.trace 'Reply', reply
89
+ client.print(BSON.serialize(reply))
90
+ else
91
+ @logger.debug "Closing client since no reply is being sent back"
92
+ @server.close
93
+ client.close
94
+ @logger.debug "Server closed"
95
+ #@thread.kill
96
+ @logger.debug "thread killed"
97
+ start(2000)
98
+ @logger.debug "Server Restarted"
99
+ break
100
+ end
101
+ end
102
+ # Disconnect from the client
103
+ client.close
104
+ @logger.debug "Disconnected from the client"
105
+ end
106
+
107
+ end
108
+
109
+ if $0 == __FILE__
110
+ SemanticLogger::Logger.default_level = :trace
111
+ SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new(STDOUT)
112
+ server = SimpleTCPServer.new(2000)
113
+ server.thread.join
114
+ end
@@ -0,0 +1,101 @@
1
+ # Allow test to be run in-place without requiring a gem install
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
+ $LOAD_PATH.unshift File.dirname(__FILE__)
4
+
5
+ require 'rubygems'
6
+ require 'test/unit'
7
+ require 'shoulda'
8
+ require 'resilient_socket'
9
+ require 'simple_tcp_server'
10
+
11
+ SemanticLogger::Logger.default_level = :trace
12
+ SemanticLogger::Logger.appenders << SemanticLogger::Appender::File.new('test.log')
13
+
14
+ # Unit Test for ResilientSocket::TCPClient
15
+ class TCPClientTest < Test::Unit::TestCase
16
+ context ResilientSocket::TCPClient do
17
+
18
+ context "without server" do
19
+ should "raise exception when cannot reach server after 5 retries" do
20
+ exception = assert_raise ResilientSocket::ConnectionFailure do
21
+ ResilientSocket::TCPClient.new(
22
+ :server => 'localhost:3300',
23
+ :connect_retry_interval => 0.1,
24
+ :connect_retry_count => 5)
25
+ end
26
+ assert_match /After 5 attempts: Errno::ECONNREFUSED/, exception.message
27
+ end
28
+
29
+ end
30
+
31
+ context "with server" do
32
+ setup do
33
+ @server = SimpleTCPServer.new(2000)
34
+ @server_name = 'localhost:2000'
35
+ end
36
+
37
+ teardown do
38
+ @server.stop if @server
39
+ end
40
+
41
+ # Not sure how to automatically test this, need a server that is running
42
+ # but does not respond in time to a connect request
43
+ #
44
+ #should "timeout on connect" do
45
+ # exception = assert_raise ResilientSocket::ConnectionTimeout do
46
+ # ResilientSocket::TCPClient.new(
47
+ # :server => @server_name,
48
+ # :connect_timeout => 0.1
49
+ # )
50
+ # end
51
+ # assert_match /Timedout after/, exception.message
52
+ #end
53
+
54
+ context "with client connection" do
55
+ setup do
56
+ @read_timeout = 3.0
57
+ @client = ResilientSocket::TCPClient.new(
58
+ :server => @server_name,
59
+ :read_timeout => @read_timeout
60
+ )
61
+ end
62
+
63
+ def teardown
64
+ @client.close if @client
65
+ end
66
+
67
+ should "successfully send and receive data" do
68
+ request = { 'action' => 'test1' }
69
+ @client.send(BSON.serialize(request))
70
+ reply = read_bson_document(@client)
71
+ assert_equal 'test1', reply['result']
72
+ end
73
+
74
+ should "timeout on receive" do
75
+ request = { 'action' => 'sleep', 'duration' => @read_timeout + 0.5}
76
+ @client.send(BSON.serialize(request))
77
+
78
+ exception = assert_raise ResilientSocket::ReadTimeout do
79
+ # Read 4 bytes from server
80
+ @client.read(4)
81
+ end
82
+ assert_match /Timedout after #{@read_timeout} seconds trying to read from #{@server_name}/, exception.message
83
+ end
84
+
85
+ should "retry on connection failure" do
86
+ attempt = 0
87
+ reply = @client.retry_on_connection_failure do
88
+ request = { 'action' => 'fail', 'attempt' => (attempt+=1) }
89
+ @client.send(BSON.serialize(request))
90
+ # Note: Do not put the read in this block if it should never send the
91
+ # same request twice to the server
92
+ read_bson_document(@client)
93
+ end
94
+ assert_equal 'fail', reply['result']
95
+ end
96
+
97
+ end
98
+ end
99
+
100
+ end
101
+ end
data/test.log ADDED
Binary file
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resilient_socket
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Reid Morrison
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: semantic_logger
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: A Resilient TCP Socket Client with built-in timeouts, retries, and logging
31
+ email:
32
+ - reidmo@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/resilient_socket/exceptions.rb
38
+ - lib/resilient_socket/tcp_client.rb
39
+ - lib/resilient_socket/version.rb
40
+ - lib/resilient_socket.rb
41
+ - LICENSE.txt
42
+ - nbproject/private/private.properties
43
+ - nbproject/private/rake-d.txt
44
+ - nbproject/project.properties
45
+ - nbproject/project.xml
46
+ - Rakefile
47
+ - README.md
48
+ - resilient_socket-0.0.1.gem
49
+ - test/simple_tcp_server.rb
50
+ - test/tcp_client_test.rb
51
+ - test.log
52
+ homepage: https://github.com/ClarityServices/resilient_socket
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.24
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: A Resilient TCP Socket Client with built-in timeouts, retries, and logging
76
+ test_files: []