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.
- data/CONTRIBUTING.md +45 -0
- data/HISTORY.rdoc +5 -0
- data/LICENSE +16 -0
- data/Manifest.txt +53 -0
- data/README.rdoc +44 -0
- data/Rakefile +337 -0
- data/example/client_test.rb +73 -0
- data/lib/kjess.rb +11 -0
- data/lib/kjess/client.rb +289 -0
- data/lib/kjess/connection.rb +119 -0
- data/lib/kjess/error.rb +5 -0
- data/lib/kjess/protocol.rb +76 -0
- data/lib/kjess/request.rb +31 -0
- data/lib/kjess/request/delete.rb +11 -0
- data/lib/kjess/request/dump_stats.rb +7 -0
- data/lib/kjess/request/flush.rb +11 -0
- data/lib/kjess/request/flush_all.rb +7 -0
- data/lib/kjess/request/get.rb +21 -0
- data/lib/kjess/request/quit.rb +7 -0
- data/lib/kjess/request/reload.rb +7 -0
- data/lib/kjess/request/set.rb +19 -0
- data/lib/kjess/request/shutdown.rb +7 -0
- data/lib/kjess/request/stats.rb +7 -0
- data/lib/kjess/request/status.rb +8 -0
- data/lib/kjess/request/version.rb +8 -0
- data/lib/kjess/response.rb +76 -0
- data/lib/kjess/response/client_error.rb +19 -0
- data/lib/kjess/response/deleted.rb +5 -0
- data/lib/kjess/response/dumped_stats.rb +48 -0
- data/lib/kjess/response/end.rb +5 -0
- data/lib/kjess/response/eof.rb +5 -0
- data/lib/kjess/response/error.rb +13 -0
- data/lib/kjess/response/flushed_all_queues.rb +6 -0
- data/lib/kjess/response/not_found.rb +5 -0
- data/lib/kjess/response/not_stored.rb +5 -0
- data/lib/kjess/response/reloaded_config.rb +6 -0
- data/lib/kjess/response/server_error.rb +18 -0
- data/lib/kjess/response/stats.rb +60 -0
- data/lib/kjess/response/stored.rb +5 -0
- data/lib/kjess/response/unknown.rb +3 -0
- data/lib/kjess/response/value.rb +26 -0
- data/lib/kjess/response/version.rb +10 -0
- data/lib/kjess/stats_cache.rb +31 -0
- data/spec/client_spec.rb +265 -0
- data/spec/kestrel_server.rb +137 -0
- data/spec/request/set_spec.rb +13 -0
- data/spec/request/version_spec.rb +17 -0
- data/spec/request_spec.rb +30 -0
- data/spec/response/client_error_spec.rb +17 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/utils.rb +18 -0
- data/spec/version_spec.rb +9 -0
- data/tasks/kestrel.rake +70 -0
- metadata +193 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
class KJess::Response
|
2
|
+
class Stats < KJess::Response
|
3
|
+
keyword 'STAT'
|
4
|
+
arity 2
|
5
|
+
|
6
|
+
attr_accessor :data
|
7
|
+
|
8
|
+
# Internal: Read the extra data from the value
|
9
|
+
#
|
10
|
+
# Read the datablock that is after the value and then the final END marker.
|
11
|
+
#
|
12
|
+
# Returns nothing
|
13
|
+
def read_more( connection )
|
14
|
+
stats = Hash.new
|
15
|
+
line = message
|
16
|
+
|
17
|
+
begin
|
18
|
+
cmd, raw_key, raw_value = line.strip.split
|
19
|
+
case cmd
|
20
|
+
when "STAT"
|
21
|
+
key = convert_key( raw_key )
|
22
|
+
value = convert_value( raw_value )
|
23
|
+
stats[key] = value
|
24
|
+
when "END"
|
25
|
+
break
|
26
|
+
else
|
27
|
+
raise KJess::Error, "Unknown line '#{line.strip}' from STAT command"
|
28
|
+
end
|
29
|
+
end while line = connection.readline
|
30
|
+
|
31
|
+
@data = stats
|
32
|
+
end
|
33
|
+
|
34
|
+
# Internal: conver the line from STATS to a valid key for the stats hash.
|
35
|
+
#
|
36
|
+
# key - the under_scored key
|
37
|
+
#
|
38
|
+
# returns the new key
|
39
|
+
def convert_key( key )
|
40
|
+
key_parts = key.split("_")
|
41
|
+
return nil if key_parts.first == "queue" and key_parts.size > 2
|
42
|
+
return key
|
43
|
+
end
|
44
|
+
|
45
|
+
# Internal: convert the given value to the Integer, Float if it should be.
|
46
|
+
#
|
47
|
+
# value - the item to convert
|
48
|
+
#
|
49
|
+
# Returns a Float, Integer or the item itself
|
50
|
+
def convert_value( value )
|
51
|
+
if value =~ /\A\d+\Z/ then
|
52
|
+
Float( value ).to_i
|
53
|
+
elsif value =~ /\A\d+\.\d+\Z/
|
54
|
+
Float( value )
|
55
|
+
else
|
56
|
+
value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class KJess::Response
|
2
|
+
class Value < KJess::Response
|
3
|
+
keyword 'VALUE'
|
4
|
+
arity 3
|
5
|
+
|
6
|
+
attr_accessor :data
|
7
|
+
|
8
|
+
def queue; args[0]; end
|
9
|
+
def flags; args[1].to_i; end
|
10
|
+
def bytes; args[2].to_i; end
|
11
|
+
|
12
|
+
# Internal: Read the extra data from the value
|
13
|
+
#
|
14
|
+
# Read the datablock that is after the value and then the final END marker.
|
15
|
+
#
|
16
|
+
# Returns nothing
|
17
|
+
def read_more( connection )
|
18
|
+
read_size = bytes + CRLF.bytesize
|
19
|
+
total_data = connection.read( read_size )
|
20
|
+
@data = total_data[0...bytes]
|
21
|
+
|
22
|
+
line = connection.readline
|
23
|
+
KJess::Response.parse( line ) # throw away the 'END' line
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module KJess
|
2
|
+
class StatsCache
|
3
|
+
|
4
|
+
attr_reader :last_stat_time
|
5
|
+
attr_reader :expiration
|
6
|
+
attr_reader :client
|
7
|
+
|
8
|
+
def initialize( client, expiration = 0 )
|
9
|
+
@client = client
|
10
|
+
@expiration = expiration
|
11
|
+
@last_stat_time = Time.at( 0 )
|
12
|
+
@stats = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def expiration_time
|
16
|
+
last_stat_time + expiration
|
17
|
+
end
|
18
|
+
|
19
|
+
def expired?
|
20
|
+
Time.now > expiration_time
|
21
|
+
end
|
22
|
+
|
23
|
+
def stats
|
24
|
+
if expired? then
|
25
|
+
@stats = client.stats!
|
26
|
+
@last_stat_time = Time.now
|
27
|
+
end
|
28
|
+
return @stats
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
#$DEBUG = true
|
4
|
+
describe KJess::Client do
|
5
|
+
before do
|
6
|
+
@client = KJess::Client.new
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
KJess::Spec.reset_server( @client )
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "connection" do
|
14
|
+
it "knows if it is connected" do
|
15
|
+
@client.ping
|
16
|
+
@client.connected?.must_equal true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "can disconnect and know it is disconnected" do
|
20
|
+
@client.ping
|
21
|
+
@client.connected?.must_equal true
|
22
|
+
@client.disconnect
|
23
|
+
@client.connected?.must_equal false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#version" do
|
28
|
+
it "knows the version of the server" do
|
29
|
+
@client.version.must_equal "2.3.4"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#stats" do
|
34
|
+
it "can see the stats on an empty server" do
|
35
|
+
@client.stats['version'].must_equal '2.3.4'
|
36
|
+
end
|
37
|
+
|
38
|
+
it "sees the stats on a server with queues" do
|
39
|
+
@client.set( 'stat_q_foo', 'stat_spec_foo' )
|
40
|
+
@client.set( 'stat_q_bar', 'stat_spec_bar' )
|
41
|
+
@client.stats['queues'].keys.sort.must_equal %w[ stat_q_bar stat_q_foo ]
|
42
|
+
end
|
43
|
+
|
44
|
+
it "has an empty queues hash when there are no queues" do
|
45
|
+
@client.stats['queues'].size.must_equal 0
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
describe "#set" do
|
51
|
+
it "adds a item to the server" do
|
52
|
+
@client.stats['curr_items'].must_equal 0
|
53
|
+
@client.set( 'set_q', "setspec" )
|
54
|
+
@client.stats['curr_items'].must_equal 1
|
55
|
+
end
|
56
|
+
|
57
|
+
it "a item with an expiration expires" do
|
58
|
+
@client.stats['curr_items'].must_equal 0
|
59
|
+
@client.set( 'set_q_2', "setspec", 1 )
|
60
|
+
@client.stats['curr_items'].must_equal 1
|
61
|
+
@client.set( 'set_q_2', "setspec2" )
|
62
|
+
@client.stats['curr_items'].must_equal 2
|
63
|
+
while s = @client.stats do
|
64
|
+
break if s['curr_items'] == 1
|
65
|
+
end
|
66
|
+
@client.get( 'set_q_2' ).must_equal 'setspec2'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#get" do
|
71
|
+
it "retrieves a item from queue" do
|
72
|
+
@client.set( 'get_q' , "a get item" )
|
73
|
+
@client.get( 'get_q' ).must_equal 'a get item'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "returns nil if no item is found" do
|
77
|
+
@client.get( 'get_q' ).must_be_nil
|
78
|
+
end
|
79
|
+
|
80
|
+
it "waits for a period of time and then times out" do
|
81
|
+
t1 = Time.now.to_f
|
82
|
+
x = @client.get( 'get_q', :wait_for => 100 )
|
83
|
+
t2 = Time.now.to_f
|
84
|
+
(t2 - t1).must_be :>=, 0.1
|
85
|
+
x.must_be_nil
|
86
|
+
end
|
87
|
+
|
88
|
+
it "raises an error if peeking and aborting" do
|
89
|
+
lambda { @client.get( 'get_q', :peek => true, :abort => true ) }.must_raise KJess::ClientError
|
90
|
+
end
|
91
|
+
|
92
|
+
it "raises an error if peeking and opening" do
|
93
|
+
lambda { @client.get( 'get_q', :peek => true, :open => true ) }.must_raise KJess::ClientError
|
94
|
+
end
|
95
|
+
|
96
|
+
it "raises an error if peeking and closing " do
|
97
|
+
lambda { @client.get( 'get_q', :peek => true, :close => true ) }.must_raise KJess::ClientError
|
98
|
+
end
|
99
|
+
|
100
|
+
it "raises an error if aborting and opening" do
|
101
|
+
lambda { @client.get( 'get_q', :peek => true, :open => true ) }.must_raise KJess::ClientError
|
102
|
+
end
|
103
|
+
|
104
|
+
it "raises an error if aborting and closing" do
|
105
|
+
lambda { @client.get( 'get_q', :peek => true, :close => true ) }.must_raise KJess::ClientError
|
106
|
+
end
|
107
|
+
|
108
|
+
it "raises an error if we attempt to non-tranactionaly get after an open transaction" do
|
109
|
+
@client.set( "get_q", "get item 1" )
|
110
|
+
@client.set( "get_q", "get item 2" )
|
111
|
+
@client.reserve( "get_q" )
|
112
|
+
lambda { @client.get( "get_q" ) }.must_raise KJess::Error
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "#reserve" do
|
118
|
+
it "reserves a item for reliable read" do
|
119
|
+
@client.set( 'reserve_q', 'a reserve item' )
|
120
|
+
@client.queue_stats( 'reserve_q' )['open_transactions'].must_equal 0
|
121
|
+
@client.reserve( 'reserve_q' ).must_equal 'a reserve item'
|
122
|
+
@client.queue_stats( 'reserve_q' )['open_transactions'].must_equal 1
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "#close_and_reserve" do
|
127
|
+
it "reserves an item for reliable read and closes an existing read" do
|
128
|
+
@client.set( 'reserve_q', 'a reserve item 1' )
|
129
|
+
@client.set( 'reserve_q', 'a reserve item 2' )
|
130
|
+
@client.queue_stats( 'reserve_q' )['open_transactions'].must_equal 0
|
131
|
+
|
132
|
+
i1 = @client.reserve( 'reserve_q' )
|
133
|
+
i1.must_equal 'a reserve item 1'
|
134
|
+
@client.queue_stats( 'reserve_q' )['open_transactions'].must_equal 1
|
135
|
+
|
136
|
+
i2 = @client.close_and_reserve( 'reserve_q' )
|
137
|
+
i2.must_equal 'a reserve item 2'
|
138
|
+
|
139
|
+
q_stats = @client.queue_stats('reserve_q')
|
140
|
+
q_stats['open_transactions'].must_equal 1
|
141
|
+
q_stats['items'].must_equal 0
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "#close" do
|
146
|
+
it "closes an existing read" do
|
147
|
+
@client.set( 'close_q', 'close item 1' )
|
148
|
+
@client.queue_stats( 'close_q' )['open_transactions'].must_equal 0
|
149
|
+
@client.reserve( 'close_q' )
|
150
|
+
@client.queue_stats( 'close_q' )['open_transactions'].must_equal 1
|
151
|
+
@client.close( 'close_q' )
|
152
|
+
@client.queue_stats( 'close_q' )['items'].must_equal 0
|
153
|
+
@client.queue_stats( 'close_q' )['open_transactions'].must_equal 0
|
154
|
+
end
|
155
|
+
|
156
|
+
it "does not return a new item from the queue" do
|
157
|
+
@client.set( 'close_q', 'close item 1' )
|
158
|
+
@client.set( 'close_q', 'close item 2' )
|
159
|
+
@client.queue_stats( 'close_q' )['open_transactions'].must_equal 0
|
160
|
+
@client.reserve( 'close_q' )
|
161
|
+
@client.queue_stats( 'close_q' )['open_transactions'].must_equal 1
|
162
|
+
@client.close( 'close_q' )
|
163
|
+
@client.queue_stats( 'close_q' )['items'].must_equal 1
|
164
|
+
@client.queue_stats( 'close_q' )['open_transactions'].must_equal 0
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe "#abort" do
|
169
|
+
it "aborts a reserved item" do
|
170
|
+
@client.set( 'abort_q', 'abort item 1' )
|
171
|
+
q_stats = @client.queue_stats('abort_q')
|
172
|
+
q_stats['items'].must_equal 1
|
173
|
+
|
174
|
+
@client.reserve( 'abort_q' )
|
175
|
+
q_stats = @client.queue_stats('abort_q')
|
176
|
+
q_stats['open_transactions'].must_equal 1
|
177
|
+
|
178
|
+
i2 = @client.abort( 'abort_q' )
|
179
|
+
q_stats = @client.queue_stats('abort_q')
|
180
|
+
q_stats['open_transactions'].must_equal 0
|
181
|
+
q_stats['items'].must_equal 1
|
182
|
+
|
183
|
+
i2.must_be_nil
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "#peek" do
|
188
|
+
it "looks at a item at the front and does not remove it" do
|
189
|
+
@client.stats['curr_items'].must_equal 0
|
190
|
+
@client.set( 'peek_q', "peekitem" )
|
191
|
+
@client.stats['curr_items'].must_equal 1
|
192
|
+
@client.peek('peek_q').must_equal 'peekitem'
|
193
|
+
@client.stats['curr_items'].must_equal 1
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "#delete" do
|
198
|
+
it "deletes a queue" do
|
199
|
+
@client.stats['queues'].size.must_equal 0
|
200
|
+
@client.set( 'delete_q_1', 'delete me' )
|
201
|
+
@client.queue_stats( 'delete_q_1' )['items'].must_equal 1
|
202
|
+
@client.delete( 'delete_q_1' )
|
203
|
+
@client.queue_stats('delete_q_1').must_be_nil
|
204
|
+
end
|
205
|
+
|
206
|
+
it "is okay to delete a queue that does not exist" do
|
207
|
+
@client.delete( 'delete_q_does_not_exist' ).must_equal true
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe "#flush" do
|
212
|
+
it "removes all the items from a queue" do
|
213
|
+
5.times { |x| @client.set( 'flush_q', "flush_me #{x}" ) }
|
214
|
+
@client.queue_stats( 'flush_q' )['items'].must_equal 5
|
215
|
+
@client.flush( 'flush_q' )
|
216
|
+
@client.queue_stats( 'flush_q' )['items'].must_equal 0
|
217
|
+
end
|
218
|
+
|
219
|
+
it "is fine with flushing a non-existant queue" do
|
220
|
+
@client.queue_stats( 'flush_q' ).must_be_nil
|
221
|
+
@client.flush( 'flush_q' ).must_equal true
|
222
|
+
@client.queue_stats( 'flush_q' ).must_be_nil
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
describe "#flush_all" do
|
227
|
+
it "removes all items from all queues" do
|
228
|
+
@client.stats['curr_items'].must_equal 0
|
229
|
+
3.times do |qx|
|
230
|
+
4.times do |ix|
|
231
|
+
@client.set( "flush_all_queue_#{qx}", "item #{qx} #{ix}" )
|
232
|
+
end
|
233
|
+
end
|
234
|
+
@client.stats['queues'].size.must_equal 3
|
235
|
+
@client.stats['curr_items'].must_equal 12
|
236
|
+
@client.flush_all
|
237
|
+
@client.stats['curr_items'].must_equal 0
|
238
|
+
@client.stats['queues'].size.must_equal 3
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "#reload" do
|
243
|
+
it "tells kestrel to reload its config" do
|
244
|
+
@client.reload.must_equal true
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
describe "#quit" do
|
249
|
+
it "disconnects from the server" do
|
250
|
+
@client.quit.must_equal true
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
describe "#status" do
|
255
|
+
it "returns the server status" do
|
256
|
+
lambda { @client.status }.must_raise KJess::ClientError
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
describe "#ping" do
|
261
|
+
it "knows if a server is up" do
|
262
|
+
@client.ping.must_equal true
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|