etcd-rb 1.0.0.pre1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +160 -13
- data/lib/etcd/client/failover.rb +109 -0
- data/lib/etcd/client/observing.rb +60 -0
- data/lib/etcd/client/protocol.rb +186 -0
- data/lib/etcd/client.rb +15 -405
- data/lib/etcd/cluster.rb +98 -0
- data/lib/etcd/constants.rb +18 -0
- data/lib/etcd/heartbeat.rb +44 -0
- data/lib/etcd/loggable.rb +13 -0
- data/lib/etcd/node.rb +45 -0
- data/lib/etcd/observer.rb +76 -0
- data/lib/etcd/requestable.rb +24 -0
- data/lib/etcd/version.rb +1 -1
- data/lib/etcd.rb +16 -3
- data/spec/etcd/client_spec.rb +34 -257
- data/spec/etcd/cluster_spec.rb +176 -0
- data/spec/etcd/node_spec.rb +58 -0
- data/spec/etcd/observer_spec.rb +163 -0
- data/spec/integration/etcd_spec.rb +43 -10
- data/spec/resources/cluster_controller.rb +19 -0
- data/spec/spec_helper.rb +3 -2
- data/spec/support/client_helper.rb +19 -0
- data/spec/support/cluster_helper.rb +75 -0
- data/spec/support/common_helper.rb +13 -0
- metadata +41 -22
data/lib/etcd/client.rb
CHANGED
@@ -1,415 +1,25 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require 'time'
|
4
|
-
require 'thread'
|
5
|
-
require 'httpclient'
|
6
|
-
require 'multi_json'
|
7
|
-
|
8
|
-
|
9
3
|
module Etcd
|
10
|
-
# A client for `etcd`. Implements all core operations (`get`, `set`, `delete`
|
11
|
-
# and `watch`) and features (TTL, atomic test-and-set, listing directories,
|
12
|
-
# etc).
|
13
|
-
#
|
14
|
-
# In addition to the core operations there are a few convenience methods for
|
15
|
-
# doing test-and-set (a.k.a. compare-and-swap, atomic update), and continuous
|
16
|
-
# watching.
|
17
|
-
#
|
18
|
-
# @note All methods that take a key or prefix as argument will prepend a slash
|
19
|
-
# to the key if it does not start with slash.
|
20
|
-
#
|
21
|
-
# @example Basic usage
|
22
|
-
# client = Etcd::Client.new
|
23
|
-
# client.set('/foo/bar', 'baz')
|
24
|
-
# client.get('/foo/bar') # => 'baz'
|
25
|
-
# client.delete('/foo/bar') # => 'baz'
|
26
|
-
#
|
27
|
-
# @example Make a key expire automatically after 5s
|
28
|
-
# client.set('/foo', 'bar', ttl: 5)
|
29
|
-
#
|
30
|
-
# @example Atomic updates
|
31
|
-
# client.set('/foo/bar', 'baz')
|
32
|
-
# # ...
|
33
|
-
# if client.update('/foo/bar', 'qux', 'baz')
|
34
|
-
# puts 'Nobody changed our data'
|
35
|
-
# end
|
36
|
-
#
|
37
|
-
# @example Listing a directory
|
38
|
-
# client.set('/foo/bar', 'baz')
|
39
|
-
# client.set('/foo/qux', 'fizz')
|
40
|
-
# client.get('/foo') # => {'/foo/bar' => 'baz', '/foo/qux' => 'fizz'}
|
41
|
-
#
|
42
|
-
# @example Getting info for a key
|
43
|
-
# client.set('/foo', 'bar', ttl: 5)
|
44
|
-
# client.info('/foo') # => {:key => '/foo',
|
45
|
-
# # :value => '/bar',
|
46
|
-
# # :expires => Time.utc(...),
|
47
|
-
# # :ttl => 4}
|
48
|
-
#
|
49
|
-
# @example Observing changes to a key
|
50
|
-
# observer = client.observe('/foo') do |value, key|
|
51
|
-
# # This will be run asynchronously
|
52
|
-
# puts "The key #{key}" changed to #{value}"
|
53
|
-
# end
|
54
|
-
# client.set('/foo/bar', 'baz') # "The key /foo/bar changed to baz" is printed
|
55
|
-
# client.set('/foo/qux', 'fizz') # "The key /foo/qux changed to fizz" is printed
|
56
|
-
# # stop receiving change notifications
|
57
|
-
# observer.cancel
|
58
|
-
#
|
59
4
|
class Client
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
# creation of the client and connection is divided into two parts to avoid
|
64
|
-
# doing network connections in the object initialization code. Many times
|
65
|
-
# you want to defer things with side-effect until the whole object graph
|
66
|
-
# has been created. {#connect} returns self so you can just chain it after
|
67
|
-
# the call to {.new}, e.g. `Client.new.connect`.
|
68
|
-
#
|
69
|
-
# You can specify a seed node to connect to using the `:uri` option (which
|
70
|
-
# defaults to 127.0.0.1:4001), but once connected the client will prefer to
|
71
|
-
# talk to the master in order to avoid unnecessary HTTP requests, and to
|
72
|
-
# make sure that get operations find the most recent value.
|
73
|
-
#
|
74
|
-
# @param [Hash] options
|
75
|
-
# @option options [String] :uri ('http://127.0.0.1:4001') The etcd host and
|
76
|
-
# port to connect to
|
77
|
-
def initialize(options={})
|
78
|
-
@seed_uri = options[:uri] || 'http://127.0.0.1:4001'
|
79
|
-
@protocol_version = 'v1'
|
80
|
-
@http_client = HTTPClient.new(agent_name: "etcd-rb/#{VERSION}")
|
81
|
-
@http_client.redirect_uri_callback = method(:handle_redirected)
|
82
|
-
end
|
83
|
-
|
84
|
-
def connect
|
85
|
-
change_uris(@seed_uri)
|
86
|
-
cache_machines
|
87
|
-
change_uris(@machines_cache.first)
|
88
|
-
self
|
89
|
-
rescue AllNodesDownError => e
|
90
|
-
raise ConnectionError, e.message, e.backtrace
|
91
|
-
end
|
92
|
-
|
93
|
-
# Sets the value of a key.
|
94
|
-
#
|
95
|
-
# Accepts an optional `:ttl` which is the number of seconds that the key
|
96
|
-
# should live before being automatically deleted.
|
97
|
-
#
|
98
|
-
# @param key [String] the key to set
|
99
|
-
# @param value [String] the value to set
|
100
|
-
# @param options [Hash]
|
101
|
-
# @option options [Fixnum] :ttl (nil) an optional time to live (in seconds)
|
102
|
-
# for the key
|
103
|
-
# @return [String] The previous value (if any)
|
104
|
-
def set(key, value, options={})
|
105
|
-
body = {:value => value}
|
106
|
-
if ttl = options[:ttl]
|
107
|
-
body[:ttl] = ttl
|
108
|
-
end
|
109
|
-
response = request(:post, uri(key), body: body)
|
110
|
-
data = MultiJson.load(response.body)
|
111
|
-
data[S_PREV_VALUE]
|
112
|
-
end
|
113
|
-
|
114
|
-
# Atomically sets the value for a key if the current value for the key
|
115
|
-
# matches the specified expected value.
|
116
|
-
#
|
117
|
-
# Returns `true` when the operation succeeds, i.e. when the specified
|
118
|
-
# expected value matches the current value. Returns `false` otherwise.
|
119
|
-
#
|
120
|
-
# Accepts an optional `:ttl` which is the number of seconds that the key
|
121
|
-
# should live before being automatically deleted.
|
122
|
-
#
|
123
|
-
# @param key [String] the key to set
|
124
|
-
# @param value [String] the value to set
|
125
|
-
# @param expected_value [String] the value to compare to the current value
|
126
|
-
# @param options [Hash]
|
127
|
-
# @option options [Fixnum] :ttl (nil) an optional time to live (in seconds)
|
128
|
-
# for the key
|
129
|
-
# @return [true, false] whether or not the operation succeeded
|
130
|
-
def update(key, value, expected_value, options={})
|
131
|
-
body = {:value => value, :prevValue => expected_value}
|
132
|
-
if ttl = options[:ttl]
|
133
|
-
body[:ttl] = ttl
|
134
|
-
end
|
135
|
-
response = request(:post, uri(key), body: body)
|
136
|
-
response.status == 200
|
137
|
-
end
|
138
|
-
|
139
|
-
# Gets the value or values for a key.
|
140
|
-
#
|
141
|
-
# If the key represents a directory with direct decendants (e.g. "/foo" for
|
142
|
-
# "/foo/bar") a hash of keys and values will be returned.
|
143
|
-
#
|
144
|
-
# @param key [String] the key or prefix to retrieve
|
145
|
-
# @return [String, Hash] the value for the key, or a hash of keys and values
|
146
|
-
# when the key is a prefix.
|
147
|
-
def get(key)
|
148
|
-
response = request(:get, uri(key))
|
149
|
-
if response.status == 200
|
150
|
-
data = MultiJson.load(response.body)
|
151
|
-
if data.is_a?(Array)
|
152
|
-
data.each_with_object({}) do |e, acc|
|
153
|
-
acc[e[S_KEY]] = e[S_VALUE]
|
154
|
-
end
|
155
|
-
else
|
156
|
-
data[S_VALUE]
|
157
|
-
end
|
158
|
-
else
|
159
|
-
nil
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# Returns info about a key, such as TTL, expiration and index.
|
164
|
-
#
|
165
|
-
# For keys with values the returned hash will include `:key`, `:value` and
|
166
|
-
# `:index`. Additionally for keys with a TTL set there will be a `:ttl` and
|
167
|
-
# `:expiration` (as a UTC `Time`).
|
168
|
-
#
|
169
|
-
# For keys that represent directories with no direct decendants (e.g. "/foo"
|
170
|
-
# for "/foo/bar/baz") the `:dir` key will have the value `true`.
|
171
|
-
#
|
172
|
-
# For keys that represent directories with direct decendants (e.g. "/foo"
|
173
|
-
# for "/foo/bar") a hash of keys and info will be returned.
|
174
|
-
#
|
175
|
-
# @param key [String] the key or prefix to retrieve
|
176
|
-
# @return [Hash] a with info about the key, the exact contents depend on
|
177
|
-
# what kind of key it is.
|
178
|
-
def info(key)
|
179
|
-
response = request(:get, uri(key))
|
180
|
-
if response.status == 200
|
181
|
-
data = MultiJson.load(response.body)
|
182
|
-
if data.is_a?(Array)
|
183
|
-
data.each_with_object({}) do |d, acc|
|
184
|
-
info = extract_info(d)
|
185
|
-
info.delete(:action)
|
186
|
-
acc[info[:key]] = info
|
187
|
-
end
|
188
|
-
else
|
189
|
-
info = extract_info(data)
|
190
|
-
info.delete(:action)
|
191
|
-
info
|
192
|
-
end
|
193
|
-
else
|
194
|
-
nil
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# Remove a key and its value.
|
199
|
-
#
|
200
|
-
# The previous value is returned, or `nil` if the key did not exist.
|
201
|
-
#
|
202
|
-
# @param key [String] the key to remove
|
203
|
-
# @return [String] the previous value, if any
|
204
|
-
def delete(key)
|
205
|
-
response = request(:delete, uri(key))
|
206
|
-
if response.status == 200
|
207
|
-
data = MultiJson.load(response.body)
|
208
|
-
data[S_PREV_VALUE]
|
209
|
-
else
|
210
|
-
nil
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
# Returns true if the specified key exists.
|
215
|
-
#
|
216
|
-
# This is a convenience method and equivalent to calling {#get} and checking
|
217
|
-
# if the value is `nil`.
|
218
|
-
#
|
219
|
-
# @return [true, false] whether or not the specified key exists
|
220
|
-
def exists?(key)
|
221
|
-
!!get(key)
|
222
|
-
end
|
223
|
-
|
224
|
-
# Watches a key or prefix and calls the given block when with any changes.
|
225
|
-
#
|
226
|
-
# This method will block until the server replies. There is no way to cancel
|
227
|
-
# the call.
|
228
|
-
#
|
229
|
-
# The parameters to the block are the value, the key and a hash of
|
230
|
-
# additional info. The info will contain the `:action` that caused the
|
231
|
-
# change (`:set`, `:delete` etc.), the `:key`, the `:value`, the `:index`,
|
232
|
-
# `:new_key` with the value `true` when a new key was created below the
|
233
|
-
# watched prefix, `:previous_value`, if any, `:ttl` and `:expiration` if
|
234
|
-
# applicable.
|
235
|
-
#
|
236
|
-
# The reason why the block parameters are in the order`value`, `key` instead
|
237
|
-
# of `key`, `value` is because you almost always want to get the new value
|
238
|
-
# when you watch, but not always the key, and most often not the info. With
|
239
|
-
# this order you can leave out the parameters you don't need.
|
240
|
-
#
|
241
|
-
# @param prefix [String] the key or prefix to watch
|
242
|
-
# @param options [Hash]
|
243
|
-
# @option options [Fixnum] :index (nil) the index to start watching from
|
244
|
-
# @yieldparam [String] value the value of the key that changed
|
245
|
-
# @yieldparam [String] key the key that changed
|
246
|
-
# @yieldparam [Hash] info the info for the key that changed
|
247
|
-
# @return [Object] the result of the given block
|
248
|
-
def watch(prefix, options={})
|
249
|
-
parameters = {}
|
250
|
-
if index = options[:index]
|
251
|
-
parameters[:index] = index
|
252
|
-
end
|
253
|
-
response = request(:get, uri(prefix, S_WATCH), query: parameters)
|
254
|
-
data = MultiJson.load(response.body)
|
255
|
-
info = extract_info(data)
|
256
|
-
yield info[:value], info[:key], info
|
257
|
-
end
|
258
|
-
|
259
|
-
# Sets up a continuous watch of a key or prefix.
|
260
|
-
#
|
261
|
-
# This method works like {#watch} (which is used behind the scenes), but
|
262
|
-
# will re-watch the key or prefix after receiving a change notificiation.
|
263
|
-
#
|
264
|
-
# When re-watching the index of the previous change notification is used,
|
265
|
-
# so no subsequent changes will be lost while a change is being processed.
|
266
|
-
#
|
267
|
-
# Unlike {#watch} this method as asynchronous. The watch handler runs in a
|
268
|
-
# separate thread (currently a new thread is created for each invocation,
|
269
|
-
# keep this in mind if you need to watch many different keys), and can be
|
270
|
-
# cancelled by calling `#cancel` on the returned object.
|
271
|
-
#
|
272
|
-
# Because of implementation details the watch handler thread will not be
|
273
|
-
# stopped directly when you call `#cancel`. The thread will be blocked until
|
274
|
-
# the next change notification (which will be ignored). This will have very
|
275
|
-
# little effect on performance since the thread will not be runnable. Unless
|
276
|
-
# you're creating lots of observers it should not matter. If you want to
|
277
|
-
# make sure you wait for the thread to stop you can call `#join` on the
|
278
|
-
# returned object.
|
279
|
-
#
|
280
|
-
# @example Creating and cancelling an observer
|
281
|
-
# observer = client.observe('/foo') do |value|
|
282
|
-
# # do something on changes
|
283
|
-
# end
|
284
|
-
# # ...
|
285
|
-
# observer.cancel
|
286
|
-
#
|
287
|
-
# @return [#cancel, #join] an observer object which you can call cancel and
|
288
|
-
# join on
|
289
|
-
def observe(prefix, &handler)
|
290
|
-
Observer.new(self, prefix, handler).tap(&:run)
|
291
|
-
end
|
5
|
+
include Etcd::Constants
|
6
|
+
include Etcd::Requestable
|
7
|
+
include Etcd::Loggable
|
292
8
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
response = request(:get, @machines_uri)
|
300
|
-
response.body.split(MACHINES_SEPARATOR_RE)
|
301
|
-
end
|
302
|
-
|
303
|
-
private
|
304
|
-
|
305
|
-
S_KEY = 'key'.freeze
|
306
|
-
S_KEYS = 'keys'.freeze
|
307
|
-
S_VALUE = 'value'.freeze
|
308
|
-
S_INDEX = 'index'.freeze
|
309
|
-
S_EXPIRATION = 'expiration'.freeze
|
310
|
-
S_TTL = 'ttl'.freeze
|
311
|
-
S_NEW_KEY = 'newKey'.freeze
|
312
|
-
S_DIR = 'dir'.freeze
|
313
|
-
S_PREV_VALUE = 'prevValue'.freeze
|
314
|
-
S_ACTION = 'action'.freeze
|
315
|
-
S_WATCH = 'watch'.freeze
|
316
|
-
S_LOCATION = 'location'.freeze
|
317
|
-
|
318
|
-
S_SLASH = '/'.freeze
|
319
|
-
MACHINES_SEPARATOR_RE = /,\s*/
|
320
|
-
|
321
|
-
def uri(key, action=S_KEYS)
|
322
|
-
key = "/#{key}" unless key.start_with?(S_SLASH)
|
323
|
-
"#{@base_uri}/#{action}#{key}"
|
324
|
-
end
|
9
|
+
attr_accessor :cluster
|
10
|
+
attr_accessor :leader
|
11
|
+
attr_accessor :seed_uris
|
12
|
+
attr_accessor :heartbeat_freq
|
13
|
+
attr_accessor :observers
|
14
|
+
attr_accessor :status # :up/:down
|
325
15
|
|
326
|
-
def
|
327
|
-
|
328
|
-
rescue HTTPClient::TimeoutError => e
|
329
|
-
old_base_uri = @base_uri
|
330
|
-
handle_leader_down
|
331
|
-
uri.sub!(old_base_uri, @base_uri)
|
332
|
-
retry
|
16
|
+
def inspect
|
17
|
+
%Q(<Etcd::Client #{seed_uris}>)
|
333
18
|
end
|
334
19
|
|
335
|
-
def extract_info(data)
|
336
|
-
info = {
|
337
|
-
:key => data[S_KEY],
|
338
|
-
:value => data[S_VALUE],
|
339
|
-
:index => data[S_INDEX],
|
340
|
-
}
|
341
|
-
expiration_s = data[S_EXPIRATION]
|
342
|
-
ttl = data[S_TTL]
|
343
|
-
previous_value = data[S_PREV_VALUE]
|
344
|
-
action_s = data[S_ACTION]
|
345
|
-
info[:expiration] = Time.iso8601(expiration_s) if expiration_s
|
346
|
-
info[:ttl] = ttl if ttl
|
347
|
-
info[:new_key] = data[S_NEW_KEY] if data.include?(S_NEW_KEY)
|
348
|
-
info[:dir] = data[S_DIR] if data.include?(S_DIR)
|
349
|
-
info[:previous_value] = previous_value if previous_value
|
350
|
-
info[:action] = action_s.downcase.to_sym if action_s
|
351
|
-
info
|
352
|
-
end
|
353
|
-
|
354
|
-
def handle_redirected(uri, response)
|
355
|
-
location = URI.parse(response.header[S_LOCATION][0])
|
356
|
-
change_uris("#{location.scheme}://#{location.host}:#{location.port}")
|
357
|
-
cache_machines
|
358
|
-
@http_client.default_redirect_uri_callback(uri, response)
|
359
|
-
end
|
360
|
-
|
361
|
-
def handle_leader_down
|
362
|
-
if @machines_cache && @machines_cache.any?
|
363
|
-
@machines_cache.reject! { |m| @base_uri.include?(m) }
|
364
|
-
change_uris(@machines_cache.shift)
|
365
|
-
else
|
366
|
-
raise AllNodesDownError, 'All known nodes are down'
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
def cache_machines
|
371
|
-
@machines_cache = machines
|
372
|
-
end
|
373
|
-
|
374
|
-
def change_uris(leader_uri, options={})
|
375
|
-
@base_uri = "#{leader_uri}/#{@protocol_version}"
|
376
|
-
@leader_uri = "#{@base_uri}/leader"
|
377
|
-
@machines_uri = "#{@base_uri}/machines"
|
378
|
-
end
|
379
|
-
|
380
|
-
# @private
|
381
|
-
class Observer
|
382
|
-
def initialize(client, prefix, handler)
|
383
|
-
@client = client
|
384
|
-
@prefix = prefix
|
385
|
-
@handler = handler
|
386
|
-
end
|
387
|
-
|
388
|
-
def run
|
389
|
-
@running = true
|
390
|
-
index = nil
|
391
|
-
@thread = Thread.start do
|
392
|
-
while @running
|
393
|
-
@client.watch(@prefix, index: index) do |value, key, info|
|
394
|
-
if @running
|
395
|
-
index = info[:index]
|
396
|
-
@handler.call(value, key, info)
|
397
|
-
end
|
398
|
-
end
|
399
|
-
end
|
400
|
-
end
|
401
|
-
self
|
402
|
-
end
|
403
|
-
|
404
|
-
def cancel
|
405
|
-
@running = false
|
406
|
-
self
|
407
|
-
end
|
408
|
-
|
409
|
-
def join
|
410
|
-
@thread.join
|
411
|
-
self
|
412
|
-
end
|
413
|
-
end
|
414
20
|
end
|
415
21
|
end
|
22
|
+
|
23
|
+
require 'etcd/client/protocol'
|
24
|
+
require 'etcd/client/observing'
|
25
|
+
require 'etcd/client/failover'
|
data/lib/etcd/cluster.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
module Etcd
|
2
|
+
class Cluster
|
3
|
+
attr_accessor :nodes
|
4
|
+
attr_accessor :seed_uri
|
5
|
+
|
6
|
+
class << self
|
7
|
+
include Etcd::Requestable
|
8
|
+
include Etcd::Constants
|
9
|
+
include Etcd::Loggable
|
10
|
+
|
11
|
+
# @example
|
12
|
+
# Etcd::Cluster.cluster_status("http://127.0.0.1:4001")
|
13
|
+
#
|
14
|
+
# @return [Array] of node attributes as [Hash]
|
15
|
+
def cluster_status(uri)
|
16
|
+
begin
|
17
|
+
logger.debug("cluster_status - from #{uri}")
|
18
|
+
data = request_data(:get, status_uri(uri))
|
19
|
+
parse_cluster_status(data)
|
20
|
+
rescue Errno::ECONNREFUSED => e
|
21
|
+
logger.debug("cluster_status - error!")
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def status_uri(uri)
|
27
|
+
"#{uri}/v1/keys/_etcd/machines/"
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_cluster_status(cluster_status_response)
|
31
|
+
cluster_status_response.map do |attrs|
|
32
|
+
node_name = attrs[S_KEY].split(S_SLASH).last
|
33
|
+
urls = attrs[S_VALUE].split(S_AND)
|
34
|
+
etcd = urls.grep(/etcd/).first.split("=").last
|
35
|
+
raft = urls.grep(/raft/).first.split("=").last
|
36
|
+
{:name => node_name, :raft => raft, :etcd => etcd}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
# @example
|
43
|
+
# Etcd::Cluster.nodes_from_uri("http://127.0.0.1:4001")
|
44
|
+
#
|
45
|
+
# @return [Array] of [Node] instances, representing cluster status
|
46
|
+
# @see #nodes_from_attributes
|
47
|
+
def nodes_from_uri(uri)
|
48
|
+
node_attributes = cluster_status(uri)
|
49
|
+
nodes_from_attributes(node_attributes)
|
50
|
+
end
|
51
|
+
|
52
|
+
def nodes_from_attributes(node_attributes)
|
53
|
+
res = node_attributes.map do |attr|
|
54
|
+
Etcd::Node.new(attr)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# creates new cluster with updated status
|
59
|
+
# is preferred way to create a cluster instance
|
60
|
+
# @example
|
61
|
+
# Etcd::Cluster.init_from_uris("http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003")
|
62
|
+
#
|
63
|
+
# @return [Cluster] instance with live data in nodes
|
64
|
+
def init_from_uris(*uris)
|
65
|
+
Array(uris).each do |uri|
|
66
|
+
if Etcd::Cluster.cluster_status(uri)
|
67
|
+
instance = Etcd::Cluster.new(uri)
|
68
|
+
instance.update_status
|
69
|
+
return instance
|
70
|
+
end
|
71
|
+
end
|
72
|
+
raise AllNodesDownError, "could not initialize a cluster from #{uris.join(", ")}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(uri)
|
77
|
+
@seed_uri = uri
|
78
|
+
end
|
79
|
+
|
80
|
+
def nodes
|
81
|
+
@nodes ||= update_status
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_status
|
85
|
+
@nodes = begin
|
86
|
+
nodes = Etcd::Cluster.nodes_from_uri(seed_uri)
|
87
|
+
nodes.map{|x| x.update_status}
|
88
|
+
nodes
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# leader instance in cluster
|
93
|
+
# @return [Node]
|
94
|
+
def leader
|
95
|
+
nodes.select{|x| x.is_leader}.first
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Etcd
|
2
|
+
module Constants
|
3
|
+
S_KEY = 'key'.freeze
|
4
|
+
S_KEYS = 'keys'.freeze
|
5
|
+
S_VALUE = 'value'.freeze
|
6
|
+
S_INDEX = 'index'.freeze
|
7
|
+
S_EXPIRATION = 'expiration'.freeze
|
8
|
+
S_TTL = 'ttl'.freeze
|
9
|
+
S_NEW_KEY = 'newKey'.freeze
|
10
|
+
S_DIR = 'dir'.freeze
|
11
|
+
S_PREV_VALUE = 'prevValue'.freeze
|
12
|
+
S_ACTION = 'action'.freeze
|
13
|
+
S_WATCH = 'watch'.freeze
|
14
|
+
S_LOCATION = 'location'.freeze
|
15
|
+
S_SLASH = '/'.freeze
|
16
|
+
S_AND = '&'.freeze
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Etcd
|
2
|
+
class Heartbeat
|
3
|
+
include Etcd::Loggable
|
4
|
+
|
5
|
+
attr_accessor :client
|
6
|
+
attr_accessor :freq
|
7
|
+
def initialize(client, freq)
|
8
|
+
@client = client
|
9
|
+
@freq = freq
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
# Initiates heartbeating the leader node in a background thread
|
14
|
+
# ensures, that observers are refreshed after leader re-election
|
15
|
+
def start_heartbeat_if_needed
|
16
|
+
logger.debug ("start_heartbeat_if_needed - enter")
|
17
|
+
return if freq == 0
|
18
|
+
return if @heartbeat_thread
|
19
|
+
@heartbeat_thread = Thread.new do
|
20
|
+
while true do
|
21
|
+
heartbeat_command
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# The command to check leader online status,
|
28
|
+
# runs in background and is resilient to failures
|
29
|
+
def heartbeat_command
|
30
|
+
logger.debug("heartbeat_command: enter ")
|
31
|
+
logger.debug(client.observers_overview.join(", "))
|
32
|
+
begin
|
33
|
+
client.refresh_observers_if_needed
|
34
|
+
client.update_cluster if client.status == :down
|
35
|
+
client.get("foo")
|
36
|
+
rescue Exception => e
|
37
|
+
client.status = :down
|
38
|
+
logger.debug "heartbeat - #{e.message} #{e.backtrace}"
|
39
|
+
end
|
40
|
+
sleep freq
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/lib/etcd/node.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Etcd
|
2
|
+
class Node
|
3
|
+
include Etcd::Constants
|
4
|
+
include Etcd::Requestable
|
5
|
+
attr_accessor :name, :etcd, :raft
|
6
|
+
# possible values: :unknown, :running, :down
|
7
|
+
attr_accessor :status
|
8
|
+
attr_accessor :is_leader
|
9
|
+
|
10
|
+
def initialize(opts={})
|
11
|
+
check_required(opts)
|
12
|
+
@name = opts[:name]
|
13
|
+
@etcd = URI.decode(opts[:etcd])
|
14
|
+
@raft = URI.decode(opts[:raft])
|
15
|
+
@status = :unknown
|
16
|
+
end
|
17
|
+
|
18
|
+
def check_required(opts)
|
19
|
+
raise ArgumentError, "etcd URL is required!" unless opts[:etcd]
|
20
|
+
end
|
21
|
+
|
22
|
+
def update_status
|
23
|
+
begin
|
24
|
+
response = request(:get, leader_uri)
|
25
|
+
@status = :running
|
26
|
+
@is_leader = (response.body == @raft)
|
27
|
+
rescue HTTPClient::TimeoutError, Errno::ECONNREFUSED => e
|
28
|
+
@status = :down
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def leader_uri
|
33
|
+
"#{@etcd}/v1/leader"
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
%Q(<#{self.class} - #{name_with_status} - #{etcd}>)
|
38
|
+
end
|
39
|
+
|
40
|
+
def name_with_status
|
41
|
+
print_status = @is_leader ? "leader" : status
|
42
|
+
"#{name} (#{print_status})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|