etcd-rb 1.0.0.pre1 → 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.
- 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
|