/
client.py
502 lines (392 loc) · 19.2 KB
/
client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
import socket
import struct
import time
import sys
import re
import random
import logging
import protocol.voldemort_client_pb2 as protocol
from xml.dom import minidom
from datetime import datetime
import serialization
##################################################################
# A Voldemort client. Each client uses a single connection to one
# Voldemort server. All routing is done server-side.
##################################################################
## Extract all the child text of the given element
def _extract_text(elm):
if elm.nodeType == minidom.Node.TEXT_NODE:
return elm.data
elif elm.nodeType == minidom.Node.ELEMENT_NODE:
text = ""
for child in elm.childNodes:
text += _extract_text(child)
return text
## Get a single child from the element, if there are multiple children, explode.
def _child(elmt, name, required=True):
children = [child for child in elmt.childNodes
if child.nodeType == minidom.Node.ELEMENT_NODE and child.tagName == name]
if not children:
if required:
raise VoldemortException("No child '%s' for element '%s'." % (name, elmt.nodeName))
else:
return None
if len(children) > 1:
raise VoldemortException("Multiple children '%s' for element '%s'." % (name, elmt.nodeName))
return children[0]
## Get the child text of a single element
def _child_text(elmt, name, required=True, default=None):
if default:
required = False
child = _child(elmt, name, required=required)
if not child:
return default
return _extract_text(child)
def _int_or_none(s):
if s is None:
return s
return int(s)
##################################################################
# A node class representing a single voldemort server in the
# cluster. The cluster itself is just a list of nodes
##################################################################
class Node:
"""A Voldemort node with the appropriate host and port information for contacting that node"""
def __init__(self, id, host, socket_port, http_port, partitions, is_available = True, last_contact = None):
self.id = id
self.host = host
self.socket_port = socket_port
self.http_port = http_port
self.partitions = partitions
self.is_available = True
if not last_contact:
self.last_contact = time.clock()
def __repr__(self):
return 'node(id = ' + str(self.id) + ', host = ' + self.host + ', socket_port = ' + str(self.socket_port) + \
', http_port = ' + str(self.http_port) + ', partitions = ' + ', '.join(map(str, self.partitions)) + ')'
@staticmethod
def parse_cluster(xml):
"""Parse the cluster.xml file and return a dictionary of the nodes in the cluster indexed by node id """
doc = minidom.parseString(xml)
nodes = {}
for curr in doc.getElementsByTagName('server'):
id = int(_child_text(curr, 'id'))
host = _child_text(curr, 'host')
http_port = int(_child_text(curr, 'http-port'))
socket_port = int(_child_text(curr, 'socket-port'))
partition_str = _child_text(curr, 'partitions')
partitions = [int(p) for p in re.split('[\s,]+', partition_str)]
nodes[id] = Node(id = id, host = host, socket_port = socket_port, http_port = http_port, partitions = partitions)
return nodes
class Store:
def __init__(self, store_node):
self.name = _child_text(store_node, "name")
self.persistence = _child_text(store_node, "persistence")
self.routing = _child_text(store_node, "routing")
self.routing_strategy = _child_text(store_node, "routing-strategy", default="consistent-routing")
self.replication_factor = int(_child_text(store_node, "replication-factor"))
self.required_reads = int(_child_text(store_node, "required-reads"))
self.preferred_reads = _int_or_none(_child_text(store_node, "preferred-reads", required=False))
self.required_writes = int(_child_text(store_node, "required-writes"))
self.preferred_writes = _int_or_none(_child_text(store_node, "preferred-writes", required=False))
key_serializer_node = _child(store_node, "key-serializer")
try:
self.key_serializer_type = _child_text(key_serializer_node, "type")
self.key_serializer = self._create_serializer(self.key_serializer_type, key_serializer_node)
except serialization.SerializationException, e:
logging.warn("Error while creating key serializer for store [%s]: %s" % (self.name, e))
self.key_serializer_type = "invalid"
self.key_serializer = serialization.UnimplementedSerializer("invalid")
value_serializer_node = _child(store_node, "value-serializer")
try:
self.value_serializer_type = _child_text(value_serializer_node, "type")
self.value_serializer = self._create_serializer(self.value_serializer_type, value_serializer_node)
except serialization.SerializationException, e:
logging.warn("Error while creating value serializer for store [%s]: %s" % (self.name, e))
self.value_serializer_type = "invalid"
self.value_serializer = serialization.UnimplementedSerializer("invalid")
def _create_serializer(self, serializer_type, serializer_node):
if serializer_type not in serialization.SERIALIZER_CLASSES:
return serialization.UnimplementedSerializer(serializer_type)
return serialization.SERIALIZER_CLASSES[serializer_type].create_from_xml(serializer_node)
@staticmethod
def parse_stores_xml(xml, store_name):
doc = minidom.parseString(xml)
store_nodes = doc.getElementsByTagName("store")
for store_node in store_nodes:
name = _child_text(store_node, "name")
if name == store_name:
return Store(store_node)
return None
class VoldemortException(Exception):
def __init__(self, msg, code = 1):
self.code = code
self.msg = msg
def __str__(self):
return repr(self.msg)
class StoreClient:
"""A simple Voldemort client. It is single-threaded and supports only server-side routing."""
def __init__(self, store_name, bootstrap_urls, reconnect_interval = 500, conflict_resolver = None):
self.store_name = store_name
self.request_count = 0
self.conflict_resolver = conflict_resolver
self.nodes, self.store = self._bootstrap_metadata(bootstrap_urls, store_name)
if not self.store:
raise VoldemortException("Cannot find store [%s] at %s" % (store_name, bootstrap_urls))
self.node_id = random.randint(0, len(self.nodes) - 1)
self.connection = None
self.node_id, self.connection = self._reconnect()
self.reconnect_interval = reconnect_interval
self.open = True
self.key_serializer = self.store.key_serializer
self.value_serializer = self.store.value_serializer
def _make_connection(self, host, port):
protocol = 'pb0'
logging.debug('Attempting to connect to ' + host + ':' + str(port))
connection = socket.socket()
connection.connect((host, port))
logging.debug('Connection succeeded, negotiating protocol')
connection.send(protocol)
resp = connection.recv(2)
if resp != 'ok':
raise VoldemortException('Server does not understand the protocol ' + protocol)
logging.debug('Protocol negotiation suceeded')
return connection
## Connect to a the next available node in the cluster
## returns a tuple of (node_id, connection)
def _reconnect(self):
num_nodes = len(self.nodes)
attempts = 0
new_node_id = self.node_id
self._close_socket(self.connection)
while attempts < num_nodes:
new_node_id = (new_node_id + 1) % num_nodes
new_node = self.nodes[new_node_id]
connection = None
try:
connection = self._make_connection(new_node.host, new_node.socket_port)
self.request_count = 0
return new_node_id, connection
except socket.error, (err_num, message):
logging.warn('Error connecting to node ' + str(new_node_id) + ': ' + message)
attempts += 1
# If we get here all nodes have failed us, explode
raise VoldemortException('Connections to all nodes failed.')
## Safely close the socket, catching and logging any exceptions
def _close_socket(self, socket):
try:
if socket:
socket.close()
except socket.error, exp:
logging.error('Error while closing socket: ' + str(exp))
## Check if the the number of requests made on this connection is greater than the reconnect interval.
## If so reconnect to a random node in the cluster. No attempt is made at preventing the reconnecting
## from going back to the same node
def _maybe_reconnect(self):
if self.request_count >= self.reconnect_interval:
logging.debug('Completed ' + str(self.request_count) + ' requests using this connection, reconnecting...')
self.node_id, self.connection = self._reconnect()
## send a request to the server using the given connection
def _send_request(self, connection, req_bytes):
connection.send(struct.pack('>i', len(req_bytes)) + req_bytes)
self.request_count += 1
## read a response from the connection
def _receive_response(self, connection):
size_bytes = connection.recv(4)
if not size_bytes:
raise VoldemortException('Connection closed')
size = struct.unpack('>i', size_bytes)[0]
bytes_read = 0
data = []
while size and bytes_read < size:
chunk = connection.recv(size - bytes_read)
bytes_read += len(chunk)
data.append(chunk)
return ''.join(data)
## Bootstrap cluster metadata from a list of urls of nodes in the cluster.
## The urls are tuples in the form (host, port).
## A dictionary of node_id => node is returned.
def _bootstrap_metadata(self, bootstrap_urls, store_name):
random.shuffle(bootstrap_urls)
for host, port in bootstrap_urls:
logging.debug('Attempting to bootstrap metadata from ' + host + ':' + str(port))
connection = None
try:
connection = self._make_connection(host, port)
cluster_xmls = self._get_with_connection(connection, 'metadata', 'cluster.xml', should_route = False)
if len(cluster_xmls) != 1:
raise VoldemortException('Expected exactly one version of the metadata but found ' + str(cluster_xmls))
nodes = Node.parse_cluster(cluster_xmls[0][0])
logging.debug('Bootstrap from ' + host + ':' + str(port) + ' succeeded, found ' + str(len(nodes)) + " nodes.")
stores_xml = self._get_with_connection(connection, 'metadata', 'stores.xml', should_route=False)[0][0]
store = Store.parse_stores_xml(stores_xml, store_name)
return nodes, store
except socket.error, (err_num, message):
logging.warn('Metadata bootstrap from ' + host + ':' + str(port) + " failed: " + message)
finally:
self._close_socket(connection)
raise VoldemortException('All bootstrap attempts failed')
## check if the server response has an error, if so throw an exception
def _check_error(self, resp):
if resp.error and resp.error.error_code != 0:
raise VoldemortException(resp.error.error_message, resp.error.error_code)
## Increment the version for a vector clock
def _increment(self, clock):
new_clock = protocol.VectorClock()
new_clock.MergeFrom(clock)
# See if we already have a version for this guy, if so increment it
for entry in new_clock.entries:
if entry.node_id == self.node_id:
entry.version += 1
return new_clock
# Otherwise add a version
entry = new_clock.entries.add()
entry.node_id = self.node_id
entry.version = 1
new_clock.timestamp = int(time.time() * 1000)
return new_clock
## Take a list of versions, and, if a conflict resolver has been given, resolve any conflicts that can be resolved
def _resolve_conflicts(self, versions):
if self.conflict_resolver and versions:
return self.conflict_resolver(versions)
else:
return versions
## Turn a protocol buffer list of versioned items into a python list of items
def _extract_versions(self, pb_versioneds):
versions = []
for versioned in pb_versioneds:
versions.append((versioned.value, versioned.version))
return self._resolve_conflicts(versions)
## A basic request wrapper, that handles reconnection logic and failures
def _execute_request(self, fun, args):
assert self.open, 'Store has been closed.'
self._maybe_reconnect()
failures = 0
num_nodes = len(self.nodes)
while failures < num_nodes:
try:
return apply(fun, args)
except socket.error, (err_num, message):
logging.warn('Error while performing ' + fun.__name__ + ' on node ' + str(self.node_id) + ': ' + message)
self.node_id, self.connection = self._reconnect()
failures += 1
raise VoldemortException('All nodes are down, ' + fun.__name__ + ' failed.')
## An internal get function that take the connection and store name as parameters. This is
## used by both the public get() method and also the metadata bootstrap process
def _get_with_connection(self, connection, store_name, key, should_route):
"""Execute get request to the given store. Returns a (value, version) pair."""
req = protocol.VoldemortRequest()
req.should_route = should_route
req.store = store_name
req.type = protocol.GET
req.get.key = key
# send request
self._send_request(connection, req.SerializeToString())
# read and parse response
resp_str = self._receive_response(connection)
resp = protocol.GetResponse()
resp.ParseFromString(resp_str)
self._check_error(resp)
return self._extract_versions(resp.versioned)
## Inner helper function for get
def _get(self, key):
return self._get_with_connection(self.connection, self.store_name, key, True)
def get(self, key):
"""Execute a get request. Returns a list of (value, version) pairs."""
raw_key = self.key_serializer.writes(key)
return [(self.value_serializer.reads(value), version)
for value, version in self._execute_request(self._get, [raw_key])]
## Inner get_all method that takes the connection and store_name as parameters
def _get_all(self, keys):
req = protocol.VoldemortRequest()
req.should_route = True
req.store = self.store_name
req.type = protocol.GET_ALL
for key in keys:
req.getAll.keys.append(key)
# send request
self._send_request(self.connection, req.SerializeToString())
# read and parse response
resp_str = self._receive_response(self.connection)
resp = protocol.GetAllResponse()
resp.ParseFromString(resp_str)
self._check_error(resp)
values = {}
for key_val in resp.values:
values[key_val.key] = self._extract_versions(key_val.versions)
return values
def get_all(self, keys):
"""Execute get request for multiple keys given as a list or tuple.
Returns a dictionary of key => [(value, version), ...] pairs."""
raw_keys = [self.key_serializer.writes(key) for key in keys]
return dict((self.key_serializer.reads(key), [(self.value_serializer.reads(value), version)
for value, version in versioned_values])
for key, versioned_values in self._execute_request(self._get_all, [raw_keys]).iteritems())
## Get the current version of the given key by doing a get request to the store
def _fetch_version(self, key):
versioned = self.get(key)
if versioned:
version = versioned[0][1]
else:
version = protocol.VectorClock()
version.timestamp = int(time.time() * 1000)
return version
def _put(self, key, value, version):
req = protocol.VoldemortRequest()
req.should_route = True
req.store = self.store_name
req.type = protocol.PUT
req.put.key = key
req.put.versioned.value = value
req.put.versioned.version.MergeFrom(version)
# send request
self._send_request(self.connection, req.SerializeToString())
# read and parse response
resp_str = self._receive_response(self.connection)
resp = protocol.PutResponse()
resp.ParseFromString(resp_str)
self._check_error(resp)
return self._increment(version)
def put(self, key, value, version = None):
"""Execute a put request using the given key and value. If no version is specified a get(key) request
will be done to get the current version. The updated version is returned."""
raw_key = self.key_serializer.writes(key)
raw_value = self.value_serializer.writes(value)
# if we don't have a version, fetch one
if not version:
version = self._fetch_version(key)
return self._execute_request(self._put, [raw_key, raw_value, version])
def maybe_put(self, key, value, version = None):
"""Execute a put request using the given key and value. If the version being put is obsolete,
no modification will be made and this function will return None. Otherwise it will return the new version."""
try:
return self.put(key, value, version)
except:
return None
def _delete(self, key, version):
req = protocol.VoldemortRequest()
req.should_route = True
req.store = self.store_name
req.type = protocol.DELETE
req.delete.key = key
req.delete.version.MergeFrom(version)
# send request
self._send_request(self.connection, req.SerializeToString())
# read and parse response
resp_str = self._receive_response(self.connection)
resp = protocol.DeleteResponse()
resp.ParseFromString(resp_str)
self._check_error(resp)
return resp.success
def delete(self, key, version = None):
"""Execute a delete request, deleting all keys up to and including the given version.
If no version is given a get(key) request will be done to find the latest version."""
raw_key = self.key_serializer.writes(key)
# if we don't have a version, fetch one
if version == None:
version = self._fetch_version(key)
return self._execute_request(self._delete, [raw_key, version])
def close(self):
"""Close the connection this store maintains."""
self.open = False
self.connection.close()