kjess 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ class KJess::Response
2
+ class NotFound < KJess::Response
3
+ keyword 'NOT_FOUND'
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class KJess::Response
2
+ class NotStored < KJess::Response
3
+ keyword 'NOT_STORED'
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class KJess::Response
2
+ class ReloadedConfig < KJess::Response
3
+ keyword 'Reloaded'
4
+ arity 1 # 'config.'
5
+ end
6
+ end
@@ -0,0 +1,18 @@
1
+ class KJess::Response
2
+ class ServerError < KJess::Response
3
+ keyword 'SERVER_ERROR'
4
+ arity 1
5
+
6
+ def message
7
+ args.join(' ')
8
+ end
9
+
10
+ def error?
11
+ true
12
+ end
13
+
14
+ def exception
15
+ raise KJess::ServerError, message
16
+ end
17
+ end
18
+ end
@@ -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,5 @@
1
+ class KJess::Response
2
+ class Stored < KJess::Response
3
+ keyword 'STORED'
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ class KJess::Response
2
+ class Unknown < KJess::Response; end
3
+ 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,10 @@
1
+ class KJess::Response
2
+ class Version < KJess::Response
3
+ keyword 'VERSION'
4
+ arity 1
5
+
6
+ def version
7
+ args.first
8
+ end
9
+ end
10
+ 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
@@ -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