resilient_socket 0.0.2

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/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: []