kjess 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -35,7 +35,8 @@ easiest way to contribute.
35
35
 
36
36
  # Contributors
37
37
 
38
- * Jeremy Hinegardner
38
+ * Jeremy Hinegardner <https://github.com/copiousfreetime>
39
+ * Eric Lindvall <https://github.com/eric>
39
40
 
40
41
  [GitHub Account]: https://github.com/signup/free "GitHub Signup"
41
42
  [GitHub Issues]: https://github.com/copiousfreetime/kjess/issues "KJess Issues"
@@ -1,4 +1,15 @@
1
1
  = KJess Changlog
2
+
3
+ == Version 1.1.0 - 2013-01-08
4
+
5
+ * Allow the setting of the kestrel server processes ports for testing (eric)
6
+ * Update development dependencies, new version and missing gems
7
+ * Re-wrap all network errors with KJess Errors (eric)
8
+ * Ensure clients are not shared across fork() (eric)
9
+ * Enforce boolean return value from KJess::Client#set (eric)
10
+ * Added socket timeout feature (eric)
11
+ * Added support for tcp keepalive
12
+
2
13
  == Version 1.0.0 - 2012-10-31
3
14
 
4
15
  * Initial Release - Yeah!
@@ -36,10 +36,12 @@ lib/kjess/response/not_stored.rb
36
36
  lib/kjess/response/reloaded_config.rb
37
37
  lib/kjess/response/server_error.rb
38
38
  lib/kjess/response/stats.rb
39
+ lib/kjess/response/status.rb
39
40
  lib/kjess/response/stored.rb
40
41
  lib/kjess/response/unknown.rb
41
42
  lib/kjess/response/value.rb
42
43
  lib/kjess/response/version.rb
44
+ lib/kjess/socket.rb
43
45
  lib/kjess/stats_cache.rb
44
46
  spec/client_spec.rb
45
47
  spec/kestrel_server.rb
data/Rakefile CHANGED
@@ -19,13 +19,7 @@ namespace :develop do
19
19
  require 'rubygems/dependency_installer'
20
20
  installer = Gem::DependencyInstaller.new
21
21
 
22
- # list these here instead of gem dependencies since there is not a way to
23
- # specify ruby version specific dependencies
24
- if RUBY_VERSION < "1.9.2"
25
- Util.platform_gemspec.add_development_dependency( 'rcov', '~> 0.9.11' )
26
- else
27
- Util.platform_gemspec.add_development_dependency( 'simplecov', '~> 0.6.4' )
28
- end
22
+ Util.set_coverage_gem
29
23
 
30
24
  puts "Installing gem depedencies needed for development"
31
25
  Util.platform_gemspec.dependencies.each do |dep|
@@ -194,14 +188,13 @@ This.gemspec['ruby'] = Gem::Specification.new do |spec|
194
188
  "--markup", "tomdoc" ]
195
189
 
196
190
  # The Runtime Dependencies
197
- # FIXME
198
- # spec.add_dependency( 'map', '~> 6.2.0')
199
191
 
200
192
  # The Development Dependencies
201
- spec.add_development_dependency( 'rake' , '~> 0.9.2.2')
202
- spec.add_development_dependency( 'minitest' , '~> 3.3.0' )
203
- spec.add_development_dependency( 'rdoc' , '~> 3.12' )
204
- spec.add_development_dependency( 'zip' , "~> 2.0.2" )
193
+ spec.add_development_dependency( 'rake' , '~> 10.0.3')
194
+ spec.add_development_dependency( 'minitest' , '~> 4.4.0' )
195
+ spec.add_development_dependency( 'rdoc' , '~> 3.12' )
196
+ spec.add_development_dependency( 'zip' , '~> 2.0.2' )
197
+ spec.add_development_dependency( 'json' , '~> 1.7.6' )
205
198
  end
206
199
 
207
200
 
@@ -211,6 +204,7 @@ This.gemspec_file = "#{This.name}.gemspec"
211
204
  # Really this is only here to support those who use bundler
212
205
  desc "Build the #{This.name}.gemspec file"
213
206
  task :gemspec do
207
+ Util.set_coverage_gem
214
208
  File.open( This.gemspec_file, "wb+" ) do |f|
215
209
  f.write Util.platform_gemspec.to_ruby
216
210
  end
@@ -261,7 +255,13 @@ end
261
255
  # Load the extra rake tasks
262
256
  #------------------------------------------------------------------------------
263
257
  $: << "." unless $:.include?(".")
264
- load 'tasks/kestrel.rake'
258
+ begin
259
+ load 'tasks/kestrel.rake'
260
+ rescue LoadError => le
261
+ Util.task_warning( 'kestrel' )
262
+ end
263
+
264
+
265
265
 
266
266
  #------------------------------------------------------------------------------
267
267
  # Rakefile Support - This is all the guts and utility methods that are
@@ -320,6 +320,19 @@ BEGIN {
320
320
  def self.platform_gemspec
321
321
  This.gemspec[This.platform]
322
322
  end
323
+
324
+ def self.set_coverage_gem
325
+ # list these here instead of gem dependencies since there is not a way to
326
+ # specify ruby version specific dependencies
327
+ g, v = 'simplecov', '~> 0.7.1'
328
+ if RUBY_VERSION < "1.9.2"
329
+ g, v = 'rcov', '~> 1.0.0'
330
+ end
331
+
332
+ if Util.platform_gemspec.dependencies.none? { |s| s.name == g } then
333
+ Util.platform_gemspec.add_development_dependency( g, v )
334
+ end
335
+ end
323
336
  end
324
337
 
325
338
  # Hold all the metadata about this project
@@ -1,5 +1,5 @@
1
1
  module KJess
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
4
4
 
5
5
  require 'kjess/connection'
@@ -30,7 +30,7 @@ module KJess
30
30
  @port = merged[:port]
31
31
  @admin_port = merged[:admin_port]
32
32
  @stats_cache = StatsCache.new( self, merged[:stats_cache_expiration] )
33
- @connection = KJess::Connection.new( host, port )
33
+ @connection = KJess::Connection.new( host, port, merged )
34
34
  end
35
35
 
36
36
  # Public: Disconnect from the Kestrel server
@@ -77,7 +77,9 @@ module KJess
77
77
  # Returns true if successful, false otherwise
78
78
  def set( queue_name, item, expiration = 0 )
79
79
  s = KJess::Request::Set.new( :queue_name => queue_name, :data => item, :expiration => expiration )
80
- send_recv( s )
80
+ resp = send_recv( s )
81
+
82
+ return KJess::Response::Stored === resp
81
83
  end
82
84
 
83
85
  # Public: Retrieve an item from the given queue
@@ -94,10 +96,18 @@ module KJess
94
96
  def get( queue_name, opts = {} )
95
97
  opts = opts.merge( :queue_name => queue_name )
96
98
  g = KJess::Request::Get.new( opts )
97
- resp = send_recv( g )
98
99
 
99
- return resp.data if KJess::Response::Value === resp
100
- return nil
100
+ if opts[:wait_for]
101
+ wait_for_in_seconds = opts[:wait_for] / 1000
102
+ else
103
+ wait_for_in_seconds = 0.1
104
+ end
105
+
106
+ connection.with_additional_read_timeout(wait_for_in_seconds) do
107
+ resp = send_recv( g )
108
+ return resp.data if KJess::Response::Value === resp
109
+ return nil
110
+ end
101
111
  end
102
112
 
103
113
  # Public: Reserve the next item on the queue
@@ -176,17 +186,27 @@ module KJess
176
186
  #
177
187
  # Returns true if the queue was flushed.
178
188
  def flush( queue_name )
179
- req = KJess::Request::Flush.new( :queue_name => queue_name )
180
- resp = send_recv( req )
181
- return KJess::Response::End === resp
189
+ # It can take a long time to flush all of the messages
190
+ # on a server, so we'll set the read timeout to something
191
+ # much higher than usual.
192
+ connection.with_additional_read_timeout(60) do
193
+ req = KJess::Request::Flush.new( :queue_name => queue_name )
194
+ resp = send_recv( req )
195
+ return KJess::Response::End === resp
196
+ end
182
197
  end
183
198
 
184
199
  # Public: Remove all items from all queues on the kestrel server
185
200
  #
186
201
  # Returns true.
187
202
  def flush_all
188
- resp = send_recv( KJess::Request::FlushAll.new )
189
- return KJess::Response::End === resp
203
+ # It can take a long time to flush all of the messages
204
+ # on a server, so we'll set the read timeout to something
205
+ # much higher than usual.
206
+ connection.with_additional_read_timeout(60) do
207
+ resp = send_recv( KJess::Request::FlushAll.new )
208
+ return KJess::Response::End === resp
209
+ end
190
210
  end
191
211
 
192
212
  # Public: Have Kestrel reload its config.
@@ -215,7 +235,7 @@ module KJess
215
235
  #
216
236
  # Returns a String.
217
237
  def status( update_to = nil )
218
- resp = send_recv( KJess::Request::Status.new( update_to ) )
238
+ resp = send_recv( KJess::Request::Status.new( :update_to => update_to ) )
219
239
  raise KJess::Error, "Status command is not supported" if KJess::Response::ClientError === resp
220
240
  return resp.message
221
241
  end
@@ -236,6 +256,8 @@ module KJess
236
256
  # Returns a Hash
237
257
  def stats!
238
258
  stats = send_recv( KJess::Request::Stats.new )
259
+ raise KJess::Error, "Problem receiving stats: #{stats.inspect}" unless KJess::Response::Stats === stats
260
+
239
261
  h = stats.data
240
262
  dump_stats = send_recv( KJess::Request::DumpStats.new )
241
263
  h['queues'] = Hash.new
@@ -253,8 +275,7 @@ module KJess
253
275
  def ping
254
276
  stats
255
277
  true
256
- rescue Errno::ECONNREFUSED => e
257
- puts e
278
+ rescue Errno::ECONNREFUSED
258
279
  false
259
280
  end
260
281
 
@@ -1,65 +1,104 @@
1
1
  require 'fcntl'
2
- require 'socket'
3
2
  require 'resolv'
4
3
  require 'resolv-replace'
5
4
  require 'kjess/error'
5
+ require 'kjess/socket'
6
6
 
7
7
  module KJess
8
8
  # Connection
9
9
  class Connection
10
10
  class Error < KJess::Error; end
11
11
 
12
- CRLF = "\r\n"
12
+ # Public: The hostname/ip address to connect to.
13
+ def host
14
+ socket.host
15
+ end
13
16
 
14
- # Public:
15
- # The hostname/ip address to connect to
16
- attr_reader :host
17
+ # Public: The port number to connect to. Default 22133
18
+ def port
19
+ socket.port
20
+ end
17
21
 
18
- # Public
19
- # The port number to connect to. Default 22133
20
- attr_reader :port
22
+ # Public: The timeout for connecting in seconds. Defaults to 2
23
+ def connect_timeout
24
+ socket.connect_timeout
25
+ end
21
26
 
22
- def initialize( host, port = 22133 )
23
- @host = host
24
- @port = Float( port ).to_i
25
- @socket = nil
27
+ # Public: The timeout for reading in seconds. Defaults to 2
28
+ def read_timeout
29
+ socket.read_timeout
26
30
  end
27
31
 
28
- # Internal: Return the raw socket that is connected to the Kestrel server
29
- #
30
- # Returns the raw socket. If the socket is not connected it will connect and
31
- # then return it.
32
- #
33
- # Returns a TCPSocket
34
- def socket
35
- return @socket if @socket and not @socket.closed?
36
- return @socket = connect()
32
+ # Public: The timeout for writing in seconds. Defaults to 2
33
+ def write_timeout
34
+ socket.write_timeout
37
35
  end
38
36
 
39
- # Internal: Create the socket we use to talk to the Kestrel server
40
- #
41
- # Returns a TCPSocket
42
- def connect
43
- sock = TCPSocket.new( host, port )
37
+ # Internal: return thekeepalive timeout
38
+ def keepalive_active?
39
+ socket.keepalive_active?
40
+ end
44
41
 
45
- # close file descriptors if we exec or something like that
46
- sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
42
+ # Internal: return the keepalive count
43
+ # The keepalive count
44
+ def keepalive_count
45
+ socket.keepalive_count
46
+ end
47
47
 
48
- # Disable Nagle's algorithm
49
- sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
48
+ # Internal: return the keepalive interval
49
+ def keepalive_interval
50
+ socket.keepalive_interval
51
+ end
50
52
 
51
- # limit only to IPv4?
52
- # addr = ::Socket.getaddrinfo(host, nil, Socket::AF_INET)
53
- # sock = ::Socket.new(::Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
54
- # saddr = ::Socket.pack_sockaddr_in(port, addr[0][3])
53
+ # Internal: return the keepalive idle
54
+ def keepalive_idle
55
+ socket.keepalive_idle
56
+ end
55
57
 
56
- # tcp keepalive
57
- # :SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all?{|c| Socket.const_defined? c}
58
- # @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
59
- # @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, keepalive[:time])
60
- # @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, keepalive[:intvl])
61
- # @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, keepalive[:probes])
62
- return sock
58
+ # TODO: make port an option at next major version number change
59
+ def initialize( host, port = 22133, options = {} )
60
+ if port.is_a?(Hash)
61
+ options = port
62
+ port = 22133
63
+ end
64
+
65
+ @options = options.dup
66
+ @options[:host] = host
67
+ @options[:port] = Float( port ).to_i
68
+ @socket = nil
69
+ @pid = nil
70
+ @read_buffer = ''
71
+ end
72
+
73
+ # Internal: Adds time to the read timeout
74
+ #
75
+ # additional_timeout - additional number of seconds to the read timeout
76
+ #
77
+ # Returns nothing
78
+ def with_additional_read_timeout(additional_timeout, &block)
79
+ old_read_timeout = socket.read_timeout
80
+ socket.read_timeout += additional_timeout
81
+ block.call
82
+ ensure
83
+ @read_timeout = old_read_timeout
84
+ end
85
+
86
+ # Internal: Return the socket that is connected to the Kestrel server
87
+ #
88
+ # Returns the socket. If the socket is not connected it will connect and
89
+ # then return it.
90
+ #
91
+ # Make sure that we close the socket if we are not the same process that
92
+ # opened that socket to begin with.
93
+ #
94
+ # Returns a KJess::Socket
95
+ def socket
96
+ close if @pid && @pid != Process.pid
97
+ return @socket if @socket and not @socket.closed?
98
+ @socket = Socket.connect( @options )
99
+ @pid = Process.pid
100
+ @read_buffer = ''
101
+ return @socket
63
102
  end
64
103
 
65
104
  # Internal: close the socket if it is not already closed
@@ -67,6 +106,7 @@ module KJess
67
106
  # Returns nothing
68
107
  def close
69
108
  @socket.close if @socket and not @socket.closed?
109
+ @read_buffer = ''
70
110
  @socket = nil
71
111
  end
72
112
 
@@ -85,8 +125,11 @@ module KJess
85
125
  #
86
126
  # Returns nothing
87
127
  def write( msg )
88
- $stderr.write "--> #{msg}" if $DEBUG
128
+ $stderr.puts "--> #{msg}" if $DEBUG
89
129
  socket.write( msg )
130
+ rescue KJess::Error
131
+ close
132
+ raise
90
133
  end
91
134
 
92
135
  # Internal: read a single line from the socket
@@ -95,25 +138,44 @@ module KJess
95
138
  #
96
139
  # Returns a String
97
140
  def readline( eom = Protocol::CRLF )
98
- while line = socket.readline( eom ) do
99
- $stderr.write "<-- #{line}" if $DEBUG
141
+ while true
142
+ while (idx = @read_buffer.index(eom)) == nil
143
+ @read_buffer << socket.readpartial(10240)
144
+ end
145
+
146
+ line = @read_buffer.slice!(0, idx + eom.length)
147
+ $stderr.puts "<-- #{line}" if $DEBUG
100
148
  break unless line.strip.length == 0
101
149
  end
102
150
  return line
151
+ rescue KJess::Error
152
+ close
153
+ raise
103
154
  rescue EOFError
104
155
  close
105
156
  return "EOF"
157
+ rescue => e
158
+ close
159
+ raise Error, "Could not read from #{host}:#{port}: #{e.class}: #{e.message}", e.backtrace
106
160
  end
107
161
 
108
162
  # Internal: Read from the socket
109
163
  #
110
- # args - this method takes the same arguments as IO#read
164
+ # nbytes - this method takes the number of bytes to read
111
165
  #
112
166
  # Returns what IO#read returns
113
- def read( *args )
114
- d = socket.read( *args )
115
- $stderr.puts "<-- #{d}" if $DEBUG
116
- return d
167
+ def read( nbytes )
168
+ while @read_buffer.length < nbytes
169
+ @read_buffer << socket.readpartial(nbytes - @read_buffer.length)
170
+ end
171
+
172
+ result = @read_buffer.slice!(0, nbytes)
173
+
174
+ $stderr.puts "<-- #{result}" if $DEBUG
175
+ return result
176
+ rescue KJess::Error
177
+ close
178
+ raise
117
179
  end
118
180
  end
119
181
  end
@@ -1,5 +1,5 @@
1
1
  module KJess
2
- # Protocl is the base class that all Kestrel requests and responses are
2
+ # Protocol is the base class that all Kestrel requests and responses are
3
3
  # developed on. it defines the DSL for creating the Request and Response
4
4
  # objects that make up the Protocol.
5
5
  #
@@ -14,11 +14,12 @@ module KJess
14
14
  #
15
15
  # Returns the name
16
16
  def keyword( name = nil )
17
+ @keyword = nil unless defined? @keyword
17
18
  if name then
18
19
  register( name )
19
20
  @keyword = name
20
21
  end
21
- @keyword
22
+ @keyword ||= nil
22
23
  end
23
24
 
24
25
  # Internal: define or return the arity of this protocol item
@@ -3,6 +3,13 @@ class KJess::Request
3
3
  class Status < KJess::Request
4
4
  keyword 'STATUS'
5
5
  arity 1
6
- #valid_responses [ KJess::Response::Eof ]
6
+ valid_responses [ KJess::Response::Status::Up, KJess::Response::Status::Down,
7
+ KJess::Response::Status::ReadOnly, KJess::Response::Status::Quiescent,
8
+ KJess::Response::End ]
9
+
10
+ def parse_options_to_args( opts )
11
+ [ opts[:update_to] ]
12
+ end
13
+
7
14
  end
8
15
  end
@@ -70,6 +70,7 @@ require 'kjess/response/not_stored'
70
70
  require 'kjess/response/reloaded_config'
71
71
  require 'kjess/response/server_error'
72
72
  require 'kjess/response/stats'
73
+ require 'kjess/response/status'
73
74
  require 'kjess/response/stored'
74
75
  require 'kjess/response/unknown'
75
76
  require 'kjess/response/value'
@@ -0,0 +1,19 @@
1
+ class KJess::Response
2
+ class Status < KJess::Response
3
+ class Up < KJess::Response::Status
4
+ keyword "UP"
5
+ end
6
+
7
+ class Down < KJess::Response::Status
8
+ keyword "DOWN"
9
+ end
10
+
11
+ class Quiescent < KJess::Response::Status
12
+ keyword "QUIESCENT"
13
+ end
14
+
15
+ class ReadOnly< KJess::Response::Status
16
+ keyword "READONLY"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,291 @@
1
+ require 'socket'
2
+
3
+ module KJess
4
+ # Socket: A specialized socket that has been configure
5
+ class Socket
6
+ class Error < KJess::Error; end
7
+ class Timeout < Error; end
8
+
9
+ # Internal:
10
+ # The timeout for reading in seconds. Defaults to 2
11
+ attr_accessor :read_timeout
12
+
13
+ # Internal:
14
+ # The timeout for connecting in seconds. Defaults to 2
15
+ attr_reader :connect_timeout
16
+
17
+ # Internal:
18
+ # The timeout for writing in seconds. Defaults to 2
19
+ attr_reader :write_timeout
20
+
21
+ # Internal:
22
+ # The host this socket is connected to
23
+ attr_reader :host
24
+
25
+ # Internal:
26
+ # The port this socket is connected to
27
+ attr_reader :port
28
+
29
+ # Internal
30
+ #
31
+ # Used for setting TCP_KEEPIDLE: overrides tcp_keepalive_time for a single
32
+ # socket.
33
+ #
34
+ # http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html
35
+ #
36
+ # tcp_keepalive_time:
37
+ #
38
+ # The interval between the last data packet sent (simple ACKs are not
39
+ # considered data) and the first keepalive probe; after the connection is
40
+ # marked to need keepalive, this counter is not used any further.
41
+ attr_reader :keepalive_idle
42
+
43
+ # Internal
44
+ #
45
+ # Used for setting TCP_KEEPINTVL: overrides tcp_keepalive_intvl for a single
46
+ # socket.
47
+ #
48
+ # http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html
49
+ #
50
+ # tcp_keepalive_intvl:
51
+ #
52
+ # The interval between subsequential keepalive probes, regardless of what
53
+ # the connection has exchanged in the meantime.
54
+ attr_reader :keepalive_interval
55
+
56
+ # Internal
57
+ #
58
+ # Used for setting TCP_KEEPCNT: overrides tcp_keepalive_probes for a single
59
+ # socket.
60
+ #
61
+ # http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html
62
+ #
63
+ # tcp_keepalive_probes:
64
+ #
65
+ # The number of unacknowledged probes to send before considering the
66
+ # connection dead and notifying the application layer.
67
+ attr_reader :keepalive_count
68
+
69
+
70
+ # Internal: Create and connect to the given location.
71
+ #
72
+ # options, same as Constructor
73
+ #
74
+ # Returns an instance of KJess::Socket
75
+ def self.connect( options = {} )
76
+ s = Socket.new( options )
77
+ s.connect
78
+ return s
79
+ end
80
+
81
+ # Internal: Creates a new KJess::Socket
82
+ def initialize( options = {} )
83
+ @host = options.fetch(:host)
84
+ @port = options.fetch(:port)
85
+ @connect_timeout = options.fetch(:connect_timeout , 2)
86
+ @read_timeout = options.fetch(:read_timeout , 2)
87
+ @write_timeout = options.fetch(:write_timeout , 2)
88
+ @keepalive_active = options.fetch(:keepalive_active , true)
89
+ @keepalive_idle = options.fetch(:keepalive_idle , 60)
90
+ @keepalive_interval = options.fetch(:keepalive_interval, 30)
91
+ @keepalive_count = options.fetch(:keepalive_count , 5)
92
+ @socket = nil
93
+ end
94
+
95
+ # Internal: Return whether or not the keepalive_active flag is set.
96
+ def keepalive_active?
97
+ @keepalive_active
98
+ end
99
+
100
+ # Internal: Low level socket allocation and option configuration
101
+ #
102
+ # Using the options from the initializer, a new ::Socket is created that
103
+ # is:
104
+ #
105
+ # TCP, IPv4 only, autoclosing on exit, nagle's algorithm is disabled and has
106
+ # TCP Keepalive options set if keepalive is supported.
107
+ #
108
+ # Returns a new ::Socket instance
109
+ def blank_socket
110
+ sock = ::Socket.new(::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
111
+
112
+ # close file descriptors if we exec
113
+ sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
114
+
115
+ # Disable Nagle's algorithm
116
+ sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
117
+
118
+ if using_keepalive? then
119
+ sock.setsockopt( ::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE , true )
120
+ sock.setsockopt( ::Socket::SOL_TCP, ::Socket::TCP_KEEPIDLE , keepalive_idle )
121
+ sock.setsockopt( ::Socket::SOL_TCP, ::Socket::TCP_KEEPINTVL, keepalive_interval)
122
+ sock.setsockopt( ::Socket::SOL_TCP, ::Socket::TCP_KEEPCNT , keepalive_count)
123
+ end
124
+
125
+ return sock
126
+ end
127
+
128
+ # Internal: Return the connected raw Socket.
129
+ #
130
+ # If the socket is closed or non-existent it will create and connect again.
131
+ #
132
+ # Returns a ::Socket
133
+ def socket
134
+ return @socket unless closed?
135
+ @socket = connect()
136
+ end
137
+
138
+ # Internal: Closes the internal ::Socket
139
+ #
140
+ # Returns nothing
141
+ def close
142
+ @socket.close unless closed?
143
+ @socket = nil
144
+ end
145
+
146
+ # Internal: Return true the socket is closed.
147
+ def closed?
148
+ return true if @socket.nil?
149
+ return true if @socket.closed?
150
+ return false
151
+ end
152
+
153
+ # Internal:
154
+ #
155
+ # Connect to the remote host in a non-blocking fashion.
156
+ #
157
+ # Raise Error if there is a failure connecting.
158
+ #
159
+ # Return the ::Socket on success
160
+ def connect
161
+ # Calculate our timeout deadline
162
+ deadline = Time.now.to_f + connect_timeout
163
+
164
+ # Lookup destination address, we only want IPv4 , TCP
165
+ addrs = ::Socket.getaddrinfo(host, port, ::Socket::AF_INET, ::Socket::SOCK_STREAM )
166
+ errors = []
167
+ conn_error = lambda { raise errors.first }
168
+ sock = nil
169
+
170
+ addrs.find( conn_error ) do |addr|
171
+ sock = connect_or_error( addr, deadline, errors )
172
+ end
173
+ return sock
174
+ end
175
+
176
+ # Internal: Connect to the destination or raise an error.
177
+ #
178
+ # Connect to the address or capture the error of the connection
179
+ #
180
+ # addr - An address returned from Socket.getaddrinfo()
181
+ # deadline - the after which we should raise a timeout error
182
+ # errors - a collection of errors to append an error too should we have one.
183
+ #
184
+ # Make an attempt to connect to the given address. If it is successful,
185
+ # return the socket.
186
+ #
187
+ # Should the connection fail, append the exception to the errors array and
188
+ # return false.
189
+ #
190
+ def connect_or_error( addr, deadline, errors )
191
+ timeout = deadline - Time.now.to_f
192
+ raise Timeout, "Could not connect to #{host}:#{port}" if timeout <= 0
193
+ return connect_nonblock( addr, timeout )
194
+ rescue Error => e
195
+ errors << e
196
+ return false
197
+ end
198
+
199
+ # Internal: Connect to the give address within the timeout.
200
+ #
201
+ # Make an attempt to connect to a single address within the given timeout.
202
+ #
203
+ # Return the ::Socket when it is connected, or raise an Error if no
204
+ # connection was possible.
205
+ def connect_nonblock( addr, timeout )
206
+ sockaddr = ::Socket.pack_sockaddr_in(addr[1], addr[3])
207
+ sock = blank_socket()
208
+ sock.connect_nonblock( sockaddr )
209
+ return sock
210
+ rescue Errno::EINPROGRESS
211
+ if IO.select(nil, [sock], nil, timeout).nil? then
212
+ raise Timeout, "Could not connect to #{host}:#{port} within #{timeout} seconds"
213
+ end
214
+ return connect_nonblock_finalize( sock, sockaddr )
215
+ rescue => ex
216
+ raise Error, "Could not connect to #{host}:#{port}: #{ex.class}: #{ex.message}", ex.backtrace
217
+ end
218
+
219
+
220
+ # Internal: Make sure that a non-blocking connect has truely connected.
221
+ #
222
+ # Ensure that the given socket is actually connected to the given adddress.
223
+ #
224
+ # Returning the socket if it is and raising an Error if it isn't.
225
+ def connect_nonblock_finalize( sock, sockaddr )
226
+ sock.connect_nonblock( sockaddr )
227
+ rescue Errno::EISCONN
228
+ return sock
229
+ rescue => ex
230
+ raise Error, "Could not connect to #{host}:#{port}: #{ex.class}: #{ex.message}", ex.backtrace
231
+ end
232
+
233
+ # Internal: say if we are using TCP Keep Alive or not
234
+ #
235
+ # We will return true if the initialization options :keepalive_active is
236
+ # set to true, and if all the constants that are necessary to use TCP keep
237
+ # alive are defined.
238
+ #
239
+ # It may be the case that on some operating systems that the constants are
240
+ # not defined, so in that case we do not want to attempt to use tcp keep
241
+ # alive if we are unable to do so in any case.
242
+ #
243
+ # Returns true or false
244
+ def using_keepalive?
245
+ using = false
246
+ if keepalive_active? then
247
+ using = [ :SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all? do |c|
248
+ ::Socket.const_defined? c
249
+ end
250
+ end
251
+ return using
252
+ end
253
+
254
+ # Internal: Read up to a maxlen of data from the socket and store it in outbuf
255
+ #
256
+ # maxlen - the maximum number of bytes to read from the socket
257
+ # outbuf - the buffer in which to store the bytes.
258
+ #
259
+ # Returns the bytes read
260
+ def readpartial(maxlen, outbuf = nil)
261
+ return socket.read_nonblock(maxlen, outbuf)
262
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNRESET
263
+ if IO.select([@socket], nil, nil, read_timeout)
264
+ retry
265
+ else
266
+ raise Timeout, "Could not read from #{host}:#{port} in #{read_timeout} seconds"
267
+ end
268
+ end
269
+
270
+ # Internal: Write the given data to the socket
271
+ #
272
+ # buf - the data to write to the socket.
273
+ #
274
+ # Raises an error if it is unable to write the data to the socket within the
275
+ # write_timeout.
276
+ #
277
+ # returns nothing
278
+ def write( buf )
279
+ until buf.length == 0
280
+ written = socket.write_nonblock(buf)
281
+ buf = buf[written, buf.length]
282
+ end
283
+ rescue Errno::EWOULDBLOCK, Errno::EINTR, Errno::EAGAIN, Errno::ECONNRESET
284
+ if IO.select(nil, [socket], nil, write_timeout)
285
+ retry
286
+ else
287
+ raise Timeout, "Could not write to #{host}:#{port} in #{write_timeout} seconds"
288
+ end
289
+ end
290
+ end
291
+ end
@@ -1,15 +1,28 @@
1
1
  require 'spec_helper'
2
2
 
3
- #$DEBUG = true
4
3
  describe KJess::Client do
5
4
  before do
6
- @client = KJess::Client.new
5
+ @client_version = "2.4.1"
6
+ @client = KJess::Spec.kjess_client()
7
7
  end
8
8
 
9
9
  after do
10
10
  KJess::Spec.reset_server( @client )
11
11
  end
12
12
 
13
+ describe "#initialize" do
14
+ it "can set keepalive parameters" do
15
+ client = KJess::Client.new( :port => KJess::Spec.memcache_port,
16
+ :keepalive_active => true,
17
+ :keepalive_interval => 1,
18
+ :keepalive_idle => 900,
19
+ :keepalive_count => 42)
20
+ client.connection.keepalive_interval.must_equal 1
21
+ client.connection.keepalive_idle.must_equal 900
22
+ client.connection.keepalive_count.must_equal 42
23
+ end
24
+ end
25
+
13
26
  describe "connection" do
14
27
  it "knows if it is connected" do
15
28
  @client.ping
@@ -26,13 +39,13 @@ describe KJess::Client do
26
39
 
27
40
  describe "#version" do
28
41
  it "knows the version of the server" do
29
- @client.version.must_equal "2.3.4"
42
+ @client.version.must_equal @client_version
30
43
  end
31
44
  end
32
45
 
33
46
  describe "#stats" do
34
47
  it "can see the stats on an empty server" do
35
- @client.stats['version'].must_equal '2.3.4'
48
+ @client.stats['version'].must_equal @client_version
36
49
  end
37
50
 
38
51
  it "sees the stats on a server with queues" do
@@ -65,6 +78,12 @@ describe KJess::Client do
65
78
  end
66
79
  @client.get( 'set_q_2' ).must_equal 'setspec2'
67
80
  end
81
+
82
+ it 'a really long binary item' do
83
+ binary = (0..255).to_a.pack('c*') * 100
84
+ @client.set 'set_bin_q', binary
85
+ @client.get('set_bin_q').must_equal binary
86
+ end
68
87
  end
69
88
 
70
89
  describe "#get" do
@@ -253,7 +272,14 @@ describe KJess::Client do
253
272
 
254
273
  describe "#status" do
255
274
  it "returns the server status" do
256
- lambda { @client.status }.must_raise KJess::ClientError
275
+ @client.status.must_equal "UP"
276
+ end
277
+
278
+ it "can change the status" do
279
+ @client.status( "readonly" ).must_equal "END"
280
+ @client.status.must_equal "READONLY"
281
+ @client.status( "up" ).must_equal "END"
282
+ @client.status.must_equal "UP"
257
283
  end
258
284
  end
259
285
 
@@ -262,4 +288,67 @@ describe KJess::Client do
262
288
  @client.ping.must_equal true
263
289
  end
264
290
  end
291
+
292
+ describe "connecting to a server on a port that isn't listening" do
293
+ it "throws an exception" do
294
+ c = KJess::Connection.new '127.0.0.1', 65521
295
+ lambda { c.socket }.must_raise KJess::Socket::Error
296
+ end
297
+ end
298
+
299
+ describe "connecting to a server that isn't responding" do
300
+ it "throws an exception" do
301
+ c = KJess::Connection.new '127.1.1.1', 65521, :timeout => 0.5
302
+ lambda { c.socket }.must_raise KJess::Socket::Timeout
303
+ end
304
+ end
305
+
306
+ describe "reading for longer than the timeout" do
307
+ it "throws an exception" do
308
+ q = Queue.new
309
+ t = Thread.new do
310
+ begin
311
+ server = TCPServer.new 65520
312
+ q.enq :go
313
+ client = server.accept
314
+ Thread.stop
315
+ ensure
316
+ server.close rescue nil
317
+ client.close rescue nil
318
+ end
319
+ end
320
+
321
+ q.deq
322
+ c = KJess::Connection.new '127.0.0.1', 65520, :timeout => 0.5
323
+
324
+ lambda { c.readline }.must_raise KJess::Socket::Timeout
325
+
326
+ t.run
327
+ t.join
328
+ end
329
+ end
330
+
331
+ describe "writing for longer than the timeout" do
332
+ it "throws an exception" do
333
+ q = Queue.new
334
+ t = Thread.new do
335
+ begin
336
+ server = TCPServer.new 65520
337
+ q.enq :go
338
+ client = server.accept
339
+ Thread.stop
340
+ ensure
341
+ server.close rescue nil
342
+ client.close rescue nil
343
+ end
344
+ end
345
+ q.deq
346
+ c = KJess::Connection.new '127.0.0.1', 65520, :timeout => 0.5
347
+
348
+ lambda { c.write('a' * 10000000) }.must_raise KJess::Socket::Timeout
349
+
350
+ t.run
351
+ t.join
352
+ end
353
+ end
265
354
  end
@@ -6,7 +6,7 @@ module KJess::Spec
6
6
 
7
7
  class << self
8
8
  def version
9
- "2.3.4"
9
+ "2.4.1"
10
10
  end
11
11
 
12
12
  def dir
@@ -18,7 +18,7 @@ module KJess::Spec
18
18
  end
19
19
 
20
20
  def jar
21
- File.join( dir, "kestrel_2.9.1-#{version}.jar" )
21
+ File.join( dir, "kestrel_2.9.2-#{version}.jar" )
22
22
  end
23
23
 
24
24
  def queue_path
@@ -47,7 +47,9 @@ import net.lag.kestrel.config._
47
47
 
48
48
  new KestrelConfig {
49
49
  listenAddress = "0.0.0.0"
50
- memcacheListenPort = 22133
50
+ memcacheListenPort = #{KJess::Spec.memcache_port}
51
+ textListenPort = #{KJess::Spec.text_port}
52
+ thriftListenPort = #{KJess::Spec.thrift_port}
51
53
 
52
54
  queuePath = "#{KJess::Spec::KestrelServer.queue_path}"
53
55
 
@@ -62,7 +64,7 @@ new KestrelConfig {
62
64
  default.maxMemorySize = 128.megabytes
63
65
  default.maxJournalSize = 1.gigabyte
64
66
 
65
- admin.httpPort = 2223
67
+ admin.httpPort = #{KJess::Spec.admin_port}
66
68
 
67
69
  admin.statsNodes = new StatsConfig {
68
70
  reporters = new TimeSeriesCollectorConfig
@@ -80,7 +82,7 @@ _EOC
80
82
  end
81
83
 
82
84
  def get_response( path )
83
- uri = URI.parse( "http://localhost:2223/#{path}" )
85
+ uri = URI.parse( "http://localhost:#{KJess::Spec.admin_port}/#{path}" )
84
86
  resp = Net::HTTP.get_response( uri )
85
87
  JSON.parse( resp.body )
86
88
  end
@@ -100,6 +102,7 @@ _EOC
100
102
  def is_running?
101
103
  return "pong" == ping
102
104
  rescue Exception => e
105
+ #$stderr.puts e
103
106
  false
104
107
  end
105
108
 
@@ -129,9 +132,9 @@ _EOC
129
132
  h = get_response( 'shutdown' )
130
133
  return h['response'] == "ok"
131
134
  rescue => e
135
+ $stderr.puts e
132
136
  false
133
137
  end
134
138
  end
135
-
136
139
  end
137
140
  end
@@ -1,8 +1,15 @@
1
1
  require 'spec_helper'
2
2
 
3
+ module KJess::Spec
4
+ class BadRequest < KJess::Request
5
+ keyword 'BADREQUEST'
6
+ arity 1
7
+ end
8
+ end
9
+
3
10
  describe KJess::ClientError do
4
11
  before do
5
- @client = KJess::Client.new
12
+ @client = KJess::Spec.kjess_client()
6
13
  end
7
14
 
8
15
  after do
@@ -10,7 +17,7 @@ describe KJess::ClientError do
10
17
  end
11
18
 
12
19
  it "raises a client error if we send an invalid command" do
13
- lambda { @client.send_recv( KJess::Request::Status.new ) }.must_raise KJess::ClientError
20
+ lambda { @client.send_recv( KJess::Spec::BadRequest.new ) }.must_raise KJess::ClientError
14
21
  end
15
22
  end
16
23
 
@@ -9,4 +9,4 @@ require 'minitest/autorun'
9
9
  require 'minitest/pride'
10
10
  require 'kjess'
11
11
  require 'utils'
12
-
12
+ require 'thread'
@@ -5,6 +5,26 @@ module KJess
5
5
  File.expand_path( "..", ROOT )
6
6
  end
7
7
 
8
+ def self.memcache_port
9
+ ENV['KJESS_MEMCACHE_PORT'] || 33122
10
+ end
11
+
12
+ def self.thrift_port
13
+ ENV['KJESS_THRIFT_PORT'] || 9992
14
+ end
15
+
16
+ def self.text_port
17
+ ENV['KJESS_TEXT_PORT'] || 9998
18
+ end
19
+
20
+ def self.admin_port
21
+ ENV['KJESS_ADMIN_PORT'] || 9999
22
+ end
23
+
24
+ def self.kjess_client
25
+ KJess::Client.new( :port => memcache_port )
26
+ end
27
+
8
28
  def self.reset_server( client )
9
29
  client.flush_all
10
30
  qlist = client.stats['queues']
@@ -7,10 +7,10 @@ namespace :kestrel do
7
7
  require 'uri'
8
8
  require 'net/http'
9
9
 
10
- url = ::URI.parse("http://robey.github.com/kestrel/download/kestrel-#{KJess::Spec::KestrelServer.erver.version}.zip")
10
+ url = ::URI.parse("http://robey.github.com/kestrel/download/kestrel-#{KJess::Spec::KestrelServer.version}.zip")
11
11
 
12
12
  puts "downloading #{url.to_s} to #{KJess::Spec::KestrelServer.zip} ..."
13
- File.open( KJess::Spec::KestrelServer.erver.zip, "wb+") do |f|
13
+ File.open( KJess::Spec::KestrelServer.zip, "wb+") do |f|
14
14
  res = Net::HTTP.get_response( url )
15
15
  f.write( res.body )
16
16
  end
metadata CHANGED
@@ -1,97 +1,106 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: kjess
3
- version: !ruby/object:Gem::Version
4
- hash: 23
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
5
  prerelease:
6
- segments:
7
- - 1
8
- - 0
9
- - 0
10
- version: 1.0.0
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Jeremy Hinegardner
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2012-10-31 00:00:00 Z
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
12
+ date: 2013-01-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
21
15
  name: rake
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
24
17
  none: false
25
- requirements:
18
+ requirements:
26
19
  - - ~>
27
- - !ruby/object:Gem::Version
28
- hash: 11
29
- segments:
30
- - 0
31
- - 9
32
- - 2
33
- - 2
34
- version: 0.9.2.2
20
+ - !ruby/object:Gem::Version
21
+ version: 10.0.3
35
22
  type: :development
36
- version_requirements: *id001
37
- - !ruby/object:Gem::Dependency
38
- name: minitest
39
23
  prerelease: false
40
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 10.0.3
30
+ - !ruby/object:Gem::Dependency
31
+ name: minitest
32
+ requirement: !ruby/object:Gem::Requirement
41
33
  none: false
42
- requirements:
34
+ requirements:
43
35
  - - ~>
44
- - !ruby/object:Gem::Version
45
- hash: 11
46
- segments:
47
- - 3
48
- - 3
49
- - 0
50
- version: 3.3.0
36
+ - !ruby/object:Gem::Version
37
+ version: 4.4.0
51
38
  type: :development
52
- version_requirements: *id002
53
- - !ruby/object:Gem::Dependency
54
- name: rdoc
55
39
  prerelease: false
56
- requirement: &id003 !ruby/object:Gem::Requirement
40
+ version_requirements: !ruby/object:Gem::Requirement
57
41
  none: false
58
- requirements:
42
+ requirements:
59
43
  - - ~>
60
- - !ruby/object:Gem::Version
61
- hash: 31
62
- segments:
63
- - 3
64
- - 12
65
- version: "3.12"
44
+ - !ruby/object:Gem::Version
45
+ version: 4.4.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: rdoc
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '3.12'
66
54
  type: :development
67
- version_requirements: *id003
68
- - !ruby/object:Gem::Dependency
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ - !ruby/object:Gem::Dependency
69
63
  name: zip
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 2.0.2
70
+ type: :development
70
71
  prerelease: false
71
- requirement: &id004 !ruby/object:Gem::Requirement
72
+ version_requirements: !ruby/object:Gem::Requirement
72
73
  none: false
73
- requirements:
74
+ requirements:
74
75
  - - ~>
75
- - !ruby/object:Gem::Version
76
- hash: 11
77
- segments:
78
- - 2
79
- - 0
80
- - 2
76
+ - !ruby/object:Gem::Version
81
77
  version: 2.0.2
78
+ - !ruby/object:Gem::Dependency
79
+ name: json
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 1.7.6
82
86
  type: :development
83
- version_requirements: *id004
84
- description: KJess is a pure ruby Kestrel client that supports Kestrel's Memcache style protocol.
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 1.7.6
94
+ description: KJess is a pure ruby Kestrel client that supports Kestrel's Memcache
95
+ style protocol.
85
96
  email: jeremy@copiousfreetime.org
86
97
  executables: []
87
-
88
98
  extensions: []
89
-
90
- extra_rdoc_files:
99
+ extra_rdoc_files:
91
100
  - HISTORY.rdoc
92
101
  - Manifest.txt
93
102
  - README.rdoc
94
- files:
103
+ files:
95
104
  - CONTRIBUTING.md
96
105
  - HISTORY.rdoc
97
106
  - LICENSE
@@ -130,10 +139,12 @@ files:
130
139
  - lib/kjess/response/reloaded_config.rb
131
140
  - lib/kjess/response/server_error.rb
132
141
  - lib/kjess/response/stats.rb
142
+ - lib/kjess/response/status.rb
133
143
  - lib/kjess/response/stored.rb
134
144
  - lib/kjess/response/unknown.rb
135
145
  - lib/kjess/response/value.rb
136
146
  - lib/kjess/response/version.rb
147
+ - lib/kjess/socket.rb
137
148
  - lib/kjess/stats_cache.rb
138
149
  - spec/client_spec.rb
139
150
  - spec/kestrel_server.rb
@@ -147,41 +158,34 @@ files:
147
158
  - tasks/kestrel.rake
148
159
  homepage: http://github.com/copiousfreetime/kjess
149
160
  licenses: []
150
-
151
161
  post_install_message:
152
- rdoc_options:
162
+ rdoc_options:
153
163
  - --main
154
164
  - README.rdoc
155
165
  - --markup
156
166
  - tomdoc
157
- require_paths:
167
+ require_paths:
158
168
  - lib
159
- required_ruby_version: !ruby/object:Gem::Requirement
169
+ required_ruby_version: !ruby/object:Gem::Requirement
160
170
  none: false
161
- requirements:
162
- - - ">="
163
- - !ruby/object:Gem::Version
164
- hash: 3
165
- segments:
166
- - 0
167
- version: "0"
168
- required_rubygems_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ! '>='
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
176
  none: false
170
- requirements:
171
- - - ">="
172
- - !ruby/object:Gem::Version
173
- hash: 3
174
- segments:
175
- - 0
176
- version: "0"
177
+ requirements:
178
+ - - ! '>='
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
177
181
  requirements: []
178
-
179
182
  rubyforge_project:
180
183
  rubygems_version: 1.8.24
181
184
  signing_key:
182
185
  specification_version: 3
183
- summary: KJess is a pure ruby Kestrel client that supports Kestrel's Memcache style protocol.
184
- test_files:
186
+ summary: KJess is a pure ruby Kestrel client that supports Kestrel's Memcache style
187
+ protocol.
188
+ test_files:
185
189
  - spec/client_spec.rb
186
190
  - spec/kestrel_server.rb
187
191
  - spec/request/set_spec.rb