errdb 3.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/CHANGELOG.md +4 -0
- data/LICENSE +20 -0
- data/README.md +19 -0
- data/TODO.md +3 -0
- data/lib/errdb.rb +397 -0
- data/lib/errdb/version.rb +3 -0
- metadata +74 -0
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Ery Lee <ery.lee@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# errdb-rb
|
2
|
+
|
3
|
+
A Ruby client library for the [Errdb](http://github.com/erylee/errdb).
|
4
|
+
|
5
|
+
## Getting started
|
6
|
+
|
7
|
+
You can connect to Errdb by instantiating the `Redis` class:
|
8
|
+
|
9
|
+
require "errdb"
|
10
|
+
|
11
|
+
errdb = Errdb.new({:host => "localhost", :port => 7272})
|
12
|
+
|
13
|
+
Once connected, you can start running commands against Errdb:
|
14
|
+
|
15
|
+
errdb.insert "key", 9999, {"metric1" => 10, "metric2" => 20}
|
16
|
+
|
17
|
+
errdb.fetch "key", "metric1,metric2", 0, 999999999
|
18
|
+
|
19
|
+
|
data/TODO.md
ADDED
data/lib/errdb.rb
ADDED
@@ -0,0 +1,397 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
require 'net/protocol'
|
6
|
+
|
7
|
+
require 'errdb/version.rb'
|
8
|
+
|
9
|
+
begin
|
10
|
+
# Try to use the SystemTimer gem instead of Ruby's timeout library
|
11
|
+
# when running on Ruby 1.8.x. See:
|
12
|
+
# http://ph7spot.com/articles/system_timer
|
13
|
+
# We don't want to bother trying to load SystemTimer on jruby,
|
14
|
+
# ruby 1.9+ and rbx.
|
15
|
+
if !defined?(RUBY_ENGINE) || (RUBY_ENGINE == 'ruby' && RUBY_VERSION < '1.9.0')
|
16
|
+
require 'system_timer'
|
17
|
+
ErrdbTimer = SystemTimer
|
18
|
+
else
|
19
|
+
require 'timeout'
|
20
|
+
ErrdbTimer = Timeout
|
21
|
+
end
|
22
|
+
rescue LoadError => e
|
23
|
+
require 'timeout'
|
24
|
+
ErrdbTimer = Timeout
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# A Ruby client library for errdb.
|
29
|
+
#
|
30
|
+
|
31
|
+
class Errdb
|
32
|
+
|
33
|
+
##
|
34
|
+
# Default options for the cache object.
|
35
|
+
|
36
|
+
DEFAULT_OPTIONS = {
|
37
|
+
:readonly => true,
|
38
|
+
:timeout => 5,
|
39
|
+
:logger => nil,
|
40
|
+
:no_reply => false,
|
41
|
+
:check_size => true,
|
42
|
+
}
|
43
|
+
|
44
|
+
##
|
45
|
+
# Default errdb port.
|
46
|
+
|
47
|
+
DEFAULT_PORT = 7272
|
48
|
+
|
49
|
+
##
|
50
|
+
# The server this client talks to. Play at your own peril.
|
51
|
+
|
52
|
+
attr_reader :host
|
53
|
+
|
54
|
+
attr_reader :port
|
55
|
+
|
56
|
+
attr_reader :server
|
57
|
+
|
58
|
+
##
|
59
|
+
# Socket timeout limit with this client, defaults to 0.5 sec.
|
60
|
+
# Set to nil to disable timeouts.
|
61
|
+
|
62
|
+
attr_reader :timeout
|
63
|
+
|
64
|
+
##
|
65
|
+
|
66
|
+
##
|
67
|
+
# Log debug/info/warn/error to the given Logger, defaults to nil.
|
68
|
+
|
69
|
+
attr_reader :logger
|
70
|
+
|
71
|
+
##
|
72
|
+
# Accepts a list of +servers+ and a list of +opts+. +servers+ may be
|
73
|
+
# omitted. See +servers=+ for acceptable server list arguments.
|
74
|
+
#
|
75
|
+
# Valid options for +opts+ are:
|
76
|
+
#
|
77
|
+
# [:namespace] Prepends this value to all keys added or retrieved.
|
78
|
+
# [:readonly] Raises an exception on cache writes when true.
|
79
|
+
# [:timeout] Time to use as the socket read timeout. Defaults to 0.5 sec,
|
80
|
+
# set to nil to disable timeouts.
|
81
|
+
# [:logger] Logger to use for info/debug output, defaults to nil
|
82
|
+
# [:no_reply] Don't bother looking for a reply for write operations (i.e. they
|
83
|
+
# become 'fire and forget'), errdb 1.2.5 and later only, speeds up
|
84
|
+
# set/add/delete/incr/decr significantly.
|
85
|
+
# [:check_size] Raises a ErrdbError if the value to be set is greater than 1 MB, which
|
86
|
+
# is the maximum key size for the standard errdb server. Defaults to true.
|
87
|
+
# [:autofix_keys] If a key is longer than 250 characters or contains spaces,
|
88
|
+
# use an SHA1 hash instead, to prevent collisions on truncated keys.
|
89
|
+
# Other options are ignored.
|
90
|
+
|
91
|
+
def initialize(options = {})
|
92
|
+
@host = options[:host] || "127.0.0.1"
|
93
|
+
@port = (options[:port] || DEFAULT_PORT).to_i
|
94
|
+
@readonly = options[:readonly] || true
|
95
|
+
@timeout = (options[:timeout] || 5).to_f
|
96
|
+
@logger = options[:logger]
|
97
|
+
@server = Server.new(self, @host, @port)
|
98
|
+
logger.info { "errdb-client #{VERSION} #{server}" } if logger
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Returns a string representation of the cache object.
|
103
|
+
|
104
|
+
def inspect
|
105
|
+
"<Errdb: %s , ro: %p>" %
|
106
|
+
[@server.inspect, @readonly]
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Returns whether there is at least one active server for the object.
|
111
|
+
|
112
|
+
def active?
|
113
|
+
@server.active?
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Returns whether or not the cache object was created read only.
|
118
|
+
|
119
|
+
def readonly?
|
120
|
+
@readonly
|
121
|
+
end
|
122
|
+
|
123
|
+
def insert(key, time, metrics)
|
124
|
+
data = (metrics.map do |k, v| "#{k}=#{v}" end).join(",")
|
125
|
+
with_socket(server) do |socket|
|
126
|
+
socket.write "insert #{key} #{time} #{data}\r\n"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
##
|
130
|
+
# Retrieves +key+ from errdb. If +raw+ is false, the value will be
|
131
|
+
# unmarshalled.
|
132
|
+
|
133
|
+
|
134
|
+
#time:a,b,c" =~ /\Atime: (.*)/
|
135
|
+
|
136
|
+
#"19199191:a,b,c" =~ /\A(\d+):(.*)/
|
137
|
+
|
138
|
+
def fetch(key, fields, start_time, end_time)
|
139
|
+
rows = []
|
140
|
+
with_socket(server) do |socket|
|
141
|
+
socket.write "fetch #{key} #{fields} #{start_time} #{end_time}\r\n"
|
142
|
+
while line = socket.gets do
|
143
|
+
raise_on_error_response! line
|
144
|
+
break if line == "END\r\n"
|
145
|
+
if line =~ /\ATIME:(.*)/ then
|
146
|
+
names = $1.strip.split(",")
|
147
|
+
elsif line =~ /\A(\d+):(.*)/ then
|
148
|
+
timestamp = $1.to_i
|
149
|
+
values = $2.strip.split(",").map {|v| v.to_f}
|
150
|
+
rows << {timestamp => values}
|
151
|
+
else
|
152
|
+
logger.warn { "unexpected line: #{line}" } if logger
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
rows
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Gets or creates a socket connected to the given server, and yields it
|
161
|
+
# to the block
|
162
|
+
#
|
163
|
+
# If a socket error (SocketError, SystemCallError, IOError) or protocol error
|
164
|
+
# (ErrdbError) is raised by the block, closes the socket, attempts to
|
165
|
+
# connect again, and retries the block (once). If an error is again raised,
|
166
|
+
# reraises it as ErrdbError.
|
167
|
+
#
|
168
|
+
# If unable to connect to the server (or if in the reconnect wait period),
|
169
|
+
# raises ErrdbError. Note that the socket connect code marks a server
|
170
|
+
# dead for a timeout period, so retrying does not apply to connection attempt
|
171
|
+
# failures (but does still apply to unexpectedly lost connections etc.).
|
172
|
+
|
173
|
+
def with_socket(server, &block)
|
174
|
+
|
175
|
+
begin
|
176
|
+
socket = server.socket
|
177
|
+
|
178
|
+
# Raise an IndexError to show this server is out of whack. If were inside
|
179
|
+
# a with_server block, we'll catch it and attempt to restart the operation.
|
180
|
+
|
181
|
+
raise IndexError, "No connection to server (#{server.status})" if socket.nil?
|
182
|
+
|
183
|
+
block.call(socket)
|
184
|
+
|
185
|
+
rescue SocketError, Errno::EAGAIN, Timeout::Error => err
|
186
|
+
logger.warn { "Socket failure: #{err.message}" } if logger
|
187
|
+
server.mark_dead(err)
|
188
|
+
handle_error(server, err)
|
189
|
+
|
190
|
+
rescue ErrdbError, SystemCallError, IOError => err
|
191
|
+
logger.warn { "Generic failure: #{err.class.name}: #{err.message}" } if logger
|
192
|
+
handle_error(server, err) if socket.nil?
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
##
|
198
|
+
# Handles +error+ from +server+.
|
199
|
+
|
200
|
+
def handle_error(server, error)
|
201
|
+
raise error if error.is_a?(ErrdbError)
|
202
|
+
server.close if server && server.status == "CONNECTED"
|
203
|
+
new_error = ErrdbError.new error.message
|
204
|
+
new_error.set_backtrace error.backtrace
|
205
|
+
raise new_error
|
206
|
+
end
|
207
|
+
|
208
|
+
def raise_on_error_response!(response)
|
209
|
+
if response =~ /\A(?:CLIENT_|SERVER_)?ERROR(.*)/
|
210
|
+
raise ErrdbError, $1.strip
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class Server
|
215
|
+
|
216
|
+
##
|
217
|
+
# The amount of time to wait before attempting to re-establish a
|
218
|
+
# connection with a server that is marked dead.
|
219
|
+
|
220
|
+
RETRY_DELAY = 30.0
|
221
|
+
|
222
|
+
##
|
223
|
+
# The host the errdb server is running on.
|
224
|
+
|
225
|
+
attr_reader :host
|
226
|
+
|
227
|
+
##
|
228
|
+
# The port the errdb server is listening on.
|
229
|
+
|
230
|
+
attr_reader :port
|
231
|
+
|
232
|
+
##
|
233
|
+
# The time of next retry if the connection is dead.
|
234
|
+
|
235
|
+
attr_reader :retry
|
236
|
+
|
237
|
+
##
|
238
|
+
# A text status string describing the state of the server.
|
239
|
+
|
240
|
+
attr_reader :status
|
241
|
+
|
242
|
+
attr_reader :logger
|
243
|
+
|
244
|
+
##
|
245
|
+
# Create a new Errdb::Server object for the errdb instance
|
246
|
+
# listening on the given host and port
|
247
|
+
|
248
|
+
def initialize(errdb, host, port = DEFAULT_PORT)
|
249
|
+
raise ArgumentError, "No host specified" if host.nil? or host.empty?
|
250
|
+
raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero?
|
251
|
+
|
252
|
+
@host = host
|
253
|
+
@port = port.to_i
|
254
|
+
|
255
|
+
@sock = nil
|
256
|
+
@retry = nil
|
257
|
+
@status = 'NOT CONNECTED'
|
258
|
+
@timeout = errdb.timeout
|
259
|
+
@logger = errdb.logger
|
260
|
+
end
|
261
|
+
|
262
|
+
##
|
263
|
+
# Return a string representation of the server object.
|
264
|
+
|
265
|
+
def inspect
|
266
|
+
"<Errdb::Server: %s:%d (%s)>" % [@host, @port, @status]
|
267
|
+
end
|
268
|
+
|
269
|
+
##
|
270
|
+
# Check whether the server connection is alive. This will cause the
|
271
|
+
# socket to attempt to connect if it isn't already connected and or if
|
272
|
+
# the server was previously marked as down and the retry time has
|
273
|
+
# been exceeded.
|
274
|
+
|
275
|
+
def alive?
|
276
|
+
!!socket
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Try to connect to the errdb server targeted by this object.
|
281
|
+
# Returns the connected socket object on success or nil on failure.
|
282
|
+
|
283
|
+
def socket
|
284
|
+
return @sock if @sock and not @sock.closed?
|
285
|
+
|
286
|
+
@sock = nil
|
287
|
+
|
288
|
+
# If the host was dead, don't retry for a while.
|
289
|
+
return if @retry and @retry > Time.now
|
290
|
+
|
291
|
+
# Attempt to connect if not already connected.
|
292
|
+
begin
|
293
|
+
@sock = connect_to(@host, @port, @timeout)
|
294
|
+
@sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
295
|
+
@retry = nil
|
296
|
+
@status = 'CONNECTED'
|
297
|
+
rescue SocketError, SystemCallError, IOError, Timeout::Error => err
|
298
|
+
logger.warn { "Unable to open socket: #{err.class.name}, #{err.message}" } if logger
|
299
|
+
mark_dead err
|
300
|
+
end
|
301
|
+
|
302
|
+
return @sock
|
303
|
+
end
|
304
|
+
|
305
|
+
def connect_to(host, port, timeout=nil)
|
306
|
+
sock = nil
|
307
|
+
if timeout
|
308
|
+
ErrdbTimer.timeout(timeout) do
|
309
|
+
sock = TCPSocket.new(host, port)
|
310
|
+
end
|
311
|
+
else
|
312
|
+
sock = TCPSocket.new(host, port)
|
313
|
+
end
|
314
|
+
|
315
|
+
io = Errdb::BufferedIO.new(sock)
|
316
|
+
io.read_timeout = timeout
|
317
|
+
# Getting reports from several customers, including 37signals,
|
318
|
+
# that the non-blocking timeouts in 1.7.5 don't seem to be reliable.
|
319
|
+
# It can't hurt to set the underlying socket timeout also, if possible.
|
320
|
+
if timeout
|
321
|
+
secs = Integer(timeout)
|
322
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
323
|
+
optval = [secs, usecs].pack("l_2")
|
324
|
+
begin
|
325
|
+
io.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
326
|
+
io.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
327
|
+
rescue Exception => ex
|
328
|
+
# Solaris, for one, does not like/support socket timeouts.
|
329
|
+
@logger.info "[errdb-client] Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}" if @logger
|
330
|
+
end
|
331
|
+
end
|
332
|
+
io
|
333
|
+
end
|
334
|
+
|
335
|
+
##
|
336
|
+
# Close the connection to the errdb server targeted by this
|
337
|
+
# object. The server is not considered dead.
|
338
|
+
|
339
|
+
def close
|
340
|
+
@sock.close if @sock && !@sock.closed?
|
341
|
+
@sock = nil
|
342
|
+
@retry = nil
|
343
|
+
@status = "NOT CONNECTED"
|
344
|
+
end
|
345
|
+
|
346
|
+
##
|
347
|
+
# Mark the server as dead and close its socket.
|
348
|
+
|
349
|
+
def mark_dead(error)
|
350
|
+
close
|
351
|
+
@retry = Time.now + RETRY_DELAY
|
352
|
+
|
353
|
+
reason = "#{error.class.name}: #{error.message}"
|
354
|
+
@status = sprintf "%s:%s DEAD (%s), will retry at %s", @host, @port, reason, @retry
|
355
|
+
@logger.info { @status } if @logger
|
356
|
+
end
|
357
|
+
|
358
|
+
end
|
359
|
+
|
360
|
+
##
|
361
|
+
# Base Errdb exception class.
|
362
|
+
|
363
|
+
class ErrdbError < RuntimeError; end
|
364
|
+
|
365
|
+
class BufferedIO < Net::BufferedIO # :nodoc:
|
366
|
+
BUFSIZE = 1024 * 16
|
367
|
+
|
368
|
+
if RUBY_VERSION < '1.9.1'
|
369
|
+
def rbuf_fill
|
370
|
+
begin
|
371
|
+
@rbuf << @io.read_nonblock(BUFSIZE)
|
372
|
+
rescue Errno::EWOULDBLOCK
|
373
|
+
retry unless @read_timeout
|
374
|
+
if IO.select([@io], nil, nil, @read_timeout)
|
375
|
+
retry
|
376
|
+
else
|
377
|
+
raise Timeout::Error, 'IO timeout'
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def setsockopt(*args)
|
384
|
+
@io.setsockopt(*args)
|
385
|
+
end
|
386
|
+
|
387
|
+
def gets
|
388
|
+
encode(readuntil("\n"))
|
389
|
+
end
|
390
|
+
|
391
|
+
def encode(str)
|
392
|
+
str
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
end
|
397
|
+
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: errdb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 7
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: "3.0"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Ery Lee
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-03-30 00:00:00 +08:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Ruby client library for Errdb(Extensibel Round Robin Database)
|
22
|
+
email: ery.lee@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- LICENSE
|
29
|
+
- README.md
|
30
|
+
- TODO.md
|
31
|
+
- CHANGELOG.md
|
32
|
+
files:
|
33
|
+
- LICENSE
|
34
|
+
- README.md
|
35
|
+
- TODO.md
|
36
|
+
- CHANGELOG.md
|
37
|
+
- lib/errdb.rb
|
38
|
+
- lib/errdb/version.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://github.com/erylee/errdb
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
hash: 3
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 3
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.4.2
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Ruby client library for Errdb
|
73
|
+
test_files: []
|
74
|
+
|