kjess 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. data/CONTRIBUTING.md +45 -0
  2. data/HISTORY.rdoc +5 -0
  3. data/LICENSE +16 -0
  4. data/Manifest.txt +53 -0
  5. data/README.rdoc +44 -0
  6. data/Rakefile +337 -0
  7. data/example/client_test.rb +73 -0
  8. data/lib/kjess.rb +11 -0
  9. data/lib/kjess/client.rb +289 -0
  10. data/lib/kjess/connection.rb +119 -0
  11. data/lib/kjess/error.rb +5 -0
  12. data/lib/kjess/protocol.rb +76 -0
  13. data/lib/kjess/request.rb +31 -0
  14. data/lib/kjess/request/delete.rb +11 -0
  15. data/lib/kjess/request/dump_stats.rb +7 -0
  16. data/lib/kjess/request/flush.rb +11 -0
  17. data/lib/kjess/request/flush_all.rb +7 -0
  18. data/lib/kjess/request/get.rb +21 -0
  19. data/lib/kjess/request/quit.rb +7 -0
  20. data/lib/kjess/request/reload.rb +7 -0
  21. data/lib/kjess/request/set.rb +19 -0
  22. data/lib/kjess/request/shutdown.rb +7 -0
  23. data/lib/kjess/request/stats.rb +7 -0
  24. data/lib/kjess/request/status.rb +8 -0
  25. data/lib/kjess/request/version.rb +8 -0
  26. data/lib/kjess/response.rb +76 -0
  27. data/lib/kjess/response/client_error.rb +19 -0
  28. data/lib/kjess/response/deleted.rb +5 -0
  29. data/lib/kjess/response/dumped_stats.rb +48 -0
  30. data/lib/kjess/response/end.rb +5 -0
  31. data/lib/kjess/response/eof.rb +5 -0
  32. data/lib/kjess/response/error.rb +13 -0
  33. data/lib/kjess/response/flushed_all_queues.rb +6 -0
  34. data/lib/kjess/response/not_found.rb +5 -0
  35. data/lib/kjess/response/not_stored.rb +5 -0
  36. data/lib/kjess/response/reloaded_config.rb +6 -0
  37. data/lib/kjess/response/server_error.rb +18 -0
  38. data/lib/kjess/response/stats.rb +60 -0
  39. data/lib/kjess/response/stored.rb +5 -0
  40. data/lib/kjess/response/unknown.rb +3 -0
  41. data/lib/kjess/response/value.rb +26 -0
  42. data/lib/kjess/response/version.rb +10 -0
  43. data/lib/kjess/stats_cache.rb +31 -0
  44. data/spec/client_spec.rb +265 -0
  45. data/spec/kestrel_server.rb +137 -0
  46. data/spec/request/set_spec.rb +13 -0
  47. data/spec/request/version_spec.rb +17 -0
  48. data/spec/request_spec.rb +30 -0
  49. data/spec/response/client_error_spec.rb +17 -0
  50. data/spec/spec_helper.rb +12 -0
  51. data/spec/utils.rb +18 -0
  52. data/spec/version_spec.rb +9 -0
  53. data/tasks/kestrel.rake +70 -0
  54. metadata +193 -0
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #require 'perftools'
4
+ require 'kjess'
5
+ require 'trollop'
6
+ require 'hitimes'
7
+
8
+ ACTIONS = %w[ produce consume clear status ].sort
9
+
10
+ options = Trollop::options do
11
+ opt :action, "Which action to perform (#{ACTIONS.join(', ')})", :default => 'produce'
12
+ opt :qname, "The name of the queue to use" , :default => 'testing'
13
+ opt :count, "How many messages to produce or consume" , :default => 1000
14
+ opt :length, "The length of the messages (only for produce)" , :default => 1024
15
+ opt :host, "The host to contact" , :default => 'localhost'
16
+ opt :port, "The port number to use" , :default => 22133
17
+ end
18
+
19
+ Trollop::die :action, "must be one of #{ACTIONS.join(', ')}" unless ACTIONS.include?( options[:action] )
20
+
21
+ trap( 'INT' ) do
22
+ puts "Closing down"
23
+ exit
24
+ end
25
+
26
+ class ClientTest
27
+ attr_reader :options
28
+ attr_reader :client
29
+ attr_reader :timer
30
+ attr_reader :item
31
+
32
+ def initialize( options )
33
+ @options = options
34
+ @client = KJess::Client.new( :host => options[:host], :port => options[:port] )
35
+ @timer = Hitimes::TimedMetric.new( options[:action] )
36
+ @item = "x" * options[:length]
37
+ end
38
+
39
+ def time_and_count( &block )
40
+ options[:count].times do |x|
41
+ timer.measure do
42
+ block.call
43
+ end
44
+ end
45
+ return timer.stats.to_hash
46
+ end
47
+
48
+ def produce
49
+ puts "Inserting #{options[:count]} items into the queue at #{options[:host]}:#{options[:port]}"
50
+ time_and_count do
51
+ client.set( options[:qname], item )
52
+ end
53
+ end
54
+
55
+ def consume
56
+ puts "Consuming #{options[:count]} items from the queue at #{options[:host]}:#{options[:port]}"
57
+ time_and_count do
58
+ client.get( options[:qname] )
59
+ end
60
+ end
61
+
62
+ def run_test
63
+ # PerfTools::CpuProfiler.start( "/tmp/#{options[:action]}_profile" ) do
64
+ send( options[:action] )
65
+ # end
66
+ end
67
+ end
68
+
69
+ test = ClientTest.new( options )
70
+ s = test.run_test
71
+ puts s.inspect
72
+
73
+
@@ -0,0 +1,11 @@
1
+ module KJess
2
+ VERSION = "1.0.0"
3
+ end
4
+
5
+ require 'kjess/connection'
6
+ require 'kjess/stats_cache'
7
+ require 'kjess/client'
8
+ require 'kjess/error'
9
+ require 'kjess/protocol'
10
+ require 'kjess/request'
11
+ require 'kjess/response'
@@ -0,0 +1,289 @@
1
+ require 'kjess/stats_cache'
2
+ module KJess
3
+ class Client
4
+ # Public: The hostname of the kestrel server to connect to
5
+ attr_reader :host
6
+
7
+ # Public: The port on hostname of the Kestrel server
8
+ attr_reader :port
9
+
10
+ # Public: The admin HTTP Port on the Kestrel server
11
+ attr_reader :admin_port
12
+
13
+ # Internal: The cache of stats
14
+ attr_reader :stats_cache
15
+
16
+ # Public: The default parameters for a client connection to a Kestrel
17
+ # server.
18
+ def self.defaults
19
+ {
20
+ :host => 'localhost',
21
+ :port => 22133,
22
+ :admin_port => 2223,
23
+ :stats_cache_expiration => 0, # number of seconds to keep stats around
24
+ }
25
+ end
26
+
27
+ def initialize( opts = {} )
28
+ merged = Client.defaults.merge( opts )
29
+ @host = merged[:host]
30
+ @port = merged[:port]
31
+ @admin_port = merged[:admin_port]
32
+ @stats_cache = StatsCache.new( self, merged[:stats_cache_expiration] )
33
+ @connection = KJess::Connection.new( host, port )
34
+ end
35
+
36
+ # Public: Disconnect from the Kestrel server
37
+ #
38
+ # Returns nothing
39
+ def disconnect
40
+ @connection.close if connected?
41
+ @connection = nil
42
+ end
43
+
44
+ # Internal: Allocate or return the existing connection to the server
45
+ #
46
+ # Returns a KJess::Connection
47
+ def connection
48
+ @connection ||= KJess::Connection.new( host, port )
49
+ end
50
+
51
+ # Public: is the client connected to a server
52
+ #
53
+ # Returns true or false
54
+ def connected?
55
+ return false if @connection.nil?
56
+ return false if @connection.closed?
57
+ return true
58
+ end
59
+
60
+ # Public: Return the version of the Kestrel Server.
61
+ #
62
+ # Return a string
63
+ # Raise Exception if there is a
64
+ def version
65
+ v = KJess::Request::Version.new
66
+ r = send_recv( v )
67
+ return r.version if Response::Version === r
68
+ raise KJess::Error, "Unexpected Response from VERSION command"
69
+ end
70
+
71
+ # Public: Add an item to the given queue
72
+ #
73
+ # queue_name - the queue to put an item on
74
+ # item - the item to put on the queue. #to_s will be called on it.
75
+ # expiration - The number of seconds from now to expire the item
76
+ #
77
+ # Returns true if successful, false otherwise
78
+ def set( queue_name, item, expiration = 0 )
79
+ s = KJess::Request::Set.new( :queue_name => queue_name, :data => item, :expiration => expiration )
80
+ send_recv( s )
81
+ end
82
+
83
+ # Public: Retrieve an item from the given queue
84
+ #
85
+ # queue_name - the name of the queue to retrieve an item from
86
+ # options - the options for retrieving the items
87
+ # :wait_for - wait for this many ms for an item on the queued(default: 0)
88
+ # :open - count this as an reliable read (default: false)
89
+ # :close - close a previous read that was retrieved with :open
90
+ # :abort - close an existing read, returning that item to the head of the queue
91
+ # :peek - return the first item on the queue, and do not remove it
92
+ #
93
+ # returns a Response
94
+ def get( queue_name, opts = {} )
95
+ opts = opts.merge( :queue_name => queue_name )
96
+ g = KJess::Request::Get.new( opts )
97
+ resp = send_recv( g )
98
+
99
+ return resp.data if KJess::Response::Value === resp
100
+ return nil
101
+ end
102
+
103
+ # Public: Reserve the next item on the queue
104
+ #
105
+ # This is a helper method to get an item from a queue and open it for
106
+ # reliable read.
107
+ #
108
+ # queue_name - the name of the queue to retrieve an item from
109
+ # options - Additional options
110
+ # :wait_for - wait for this many ms for an item on the queue(default: 0)
111
+ def reserve( queue_name, opts = {} )
112
+ opts = opts.merge( :open => true )
113
+ get( queue_name, opts )
114
+ end
115
+
116
+ # Public: Reserve the next item on the queue and close out the previous
117
+ # read.
118
+ #
119
+ # This is a helper method to do a reliable read on a queue item while
120
+ # closing out the existing read at the same time.
121
+ #
122
+ # queue_name - the name of the quee to retieve and item from
123
+ # options - Additional options
124
+ # :wait_for - wait for this many ms for an item on the queue(default: 0)
125
+ def close_and_reserve( queue_name, opts = {} )
126
+ opts = opts.merge( :close => true )
127
+ reserve( queue_name, opts )
128
+ end
129
+
130
+ # Public: Peek at the top item in the queue
131
+ #
132
+ # queue_name - the name of the queue to retrieve an item from
133
+ #
134
+ # Returns a Response
135
+ def peek( queue_name )
136
+ get( queue_name, :peek => true )
137
+ end
138
+
139
+ # Public: Close an existing reliable read
140
+ #
141
+ # queue_name - the name of the queue to abort
142
+ #
143
+ # Returns a Response
144
+ def close( queue_name )
145
+ get( queue_name, :close => true )
146
+ end
147
+
148
+
149
+ # Public: Abort an existing reliable read
150
+ #
151
+ # queue_name - the name of the queue to abort
152
+ #
153
+ # Returns a Response
154
+ def abort( queue_name )
155
+ get( queue_name, :abort => true )
156
+ end
157
+
158
+ # Public : Remove a queue from the kestrel server
159
+ #
160
+ # This will remove any queue you want. Including queues that do not exist.
161
+ #
162
+ # queue_name - the name of the queue to remove
163
+ #
164
+ # Returns true if it was deleted false otherwise
165
+ def delete( queue_name )
166
+ req = KJess::Request::Delete.new( :queue_name => queue_name )
167
+ resp = send_recv( req )
168
+ return KJess::Response::Deleted === resp
169
+ end
170
+
171
+ # Public: Remove all items from a queue on the kestrel server
172
+ #
173
+ # This will flush any and all queue. Even queues that do not exist.
174
+ #
175
+ # queue_name - the name of the queue to flush
176
+ #
177
+ # Returns true if the queue was flushed.
178
+ 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
182
+ end
183
+
184
+ # Public: Remove all items from all queues on the kestrel server
185
+ #
186
+ # Returns true.
187
+ def flush_all
188
+ resp = send_recv( KJess::Request::FlushAll.new )
189
+ return KJess::Response::End === resp
190
+ end
191
+
192
+ # Public: Have Kestrel reload its config.
193
+ #
194
+ # Currently the kestrel server will say that the config was reloaded no
195
+ # matter what so there is no way to determine if the config failed to load.
196
+ #
197
+ # Returns true
198
+ def reload
199
+ resp = send_recv( KJess::Request::Reload.new )
200
+ return KJess::Response::ReloadedConfig === resp
201
+ end
202
+
203
+ # Public: Disconnect from the kestrel server.
204
+ #
205
+ # Returns true
206
+ def quit
207
+ resp = send_recv( KJess::Request::Quit.new )
208
+ return KJess::Response::Eof === resp
209
+ end
210
+
211
+ # Public: Return the server status.
212
+ #
213
+ # Currently this is only supported in the HEAD versin of kestrel. Version
214
+ # where this is not available will raise ServerError.
215
+ #
216
+ # Returns a String.
217
+ def status( update_to = nil )
218
+ resp = send_recv( KJess::Request::Status.new( update_to ) )
219
+ raise KJess::Error, "Status command is not supported" if KJess::Response::ClientError === resp
220
+ return resp.message
221
+ end
222
+
223
+ # Public: Return stats about the Kestrel server, they will be cached
224
+ # according to the stats_cache_expiration initialization parameter
225
+ #
226
+ # Returns a Hash
227
+ def stats
228
+ stats_cache.stats
229
+ end
230
+
231
+ # Internal: Return the hash of stats
232
+ #
233
+ # Using a combination of the STATS and DUMP_STATS commands this generates a
234
+ # good overview of all the most used stats for a Kestrel server.
235
+ #
236
+ # Returns a Hash
237
+ def stats!
238
+ stats = send_recv( KJess::Request::Stats.new )
239
+ h = stats.data
240
+ dump_stats = send_recv( KJess::Request::DumpStats.new )
241
+ h['queues'] = Hash.new
242
+ if KJess::Response::DumpedStats === dump_stats then
243
+ h['queues'].merge!( dump_stats.data )
244
+ end
245
+ return h
246
+ end
247
+
248
+ # Public: Returns true if the server is alive
249
+ #
250
+ # This uses the 'stats' method to see if the server is alive
251
+ #
252
+ # Returns true or false
253
+ def ping
254
+ stats
255
+ true
256
+ rescue Errno::ECONNREFUSED => e
257
+ puts e
258
+ false
259
+ end
260
+
261
+ # Public: Return just the stats about a particular queue
262
+ #
263
+ # Returns a Hash
264
+ def queue_stats( queue_name )
265
+ stats['queues'][queue_name]
266
+ end
267
+
268
+ # Public: Tells the Kestrel server to shutdown
269
+ #
270
+ # Returns nothing
271
+ def shutdown
272
+ send_recv( KJess::Request::Shutdown.new )
273
+ end
274
+
275
+ # Internal: Send and recive a request/response
276
+ #
277
+ # request - the Request objec to send to the server
278
+ #
279
+ # Returns a Response object
280
+ def send_recv( request )
281
+ connection.write( request.to_protocol )
282
+ line = connection.readline
283
+ resp = KJess::Response.parse( line )
284
+ resp.read_more( connection )
285
+ raise resp if resp.error?
286
+ return resp
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,119 @@
1
+ require 'fcntl'
2
+ require 'socket'
3
+ require 'resolv'
4
+ require 'resolv-replace'
5
+ require 'kjess/error'
6
+
7
+ module KJess
8
+ # Connection
9
+ class Connection
10
+ class Error < KJess::Error; end
11
+
12
+ CRLF = "\r\n"
13
+
14
+ # Public:
15
+ # The hostname/ip address to connect to
16
+ attr_reader :host
17
+
18
+ # Public
19
+ # The port number to connect to. Default 22133
20
+ attr_reader :port
21
+
22
+ def initialize( host, port = 22133 )
23
+ @host = host
24
+ @port = Float( port ).to_i
25
+ @socket = nil
26
+ end
27
+
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()
37
+ end
38
+
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 )
44
+
45
+ # close file descriptors if we exec or something like that
46
+ sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
47
+
48
+ # Disable Nagle's algorithm
49
+ sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
50
+
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])
55
+
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
63
+ end
64
+
65
+ # Internal: close the socket if it is not already closed
66
+ #
67
+ # Returns nothing
68
+ def close
69
+ @socket.close if @socket and not @socket.closed?
70
+ @socket = nil
71
+ end
72
+
73
+ # Internal: is the socket closed
74
+ #
75
+ # Returns true or false
76
+ def closed?
77
+ return true if @socket.nil?
78
+ return true if @socket.closed?
79
+ return false
80
+ end
81
+
82
+ # Internal: write the given item to the socket
83
+ #
84
+ # msg - the message to write
85
+ #
86
+ # Returns nothing
87
+ def write( msg )
88
+ $stderr.write "--> #{msg}" if $DEBUG
89
+ socket.write( msg )
90
+ end
91
+
92
+ # Internal: read a single line from the socket
93
+ #
94
+ # eom - the End Of Mesasge delimiter (default: "\r\n")
95
+ #
96
+ # Returns a String
97
+ def readline( eom = Protocol::CRLF )
98
+ while line = socket.readline( eom ) do
99
+ $stderr.write "<-- #{line}" if $DEBUG
100
+ break unless line.strip.length == 0
101
+ end
102
+ return line
103
+ rescue EOFError
104
+ close
105
+ return "EOF"
106
+ end
107
+
108
+ # Internal: Read from the socket
109
+ #
110
+ # args - this method takes the same arguments as IO#read
111
+ #
112
+ # Returns what IO#read returns
113
+ def read( *args )
114
+ d = socket.read( *args )
115
+ $stderr.puts "<-- #{d}" if $DEBUG
116
+ return d
117
+ end
118
+ end
119
+ end