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,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