def connect(self): """ Establishes a connection and starts an SSYNC request with the object server. """ with exceptions.MessageTimeout(self.daemon.conn_timeout, 'connect send'): self.connection = bufferedhttp.BufferedHTTPConnection( '%s:%s' % (self.node['replication_ip'], self.node['replication_port'])) self.connection.putrequest( 'SSYNC', '/%s/%s' % (self.node['device'], self.job['partition'])) self.connection.putheader('Transfer-Encoding', 'chunked') self.connection.putheader('X-Backend-Storage-Policy-Index', int(self.job['policy'])) # a sync job must use the node's index for the frag_index of the # rebuilt fragments instead of the frag_index from the job which # will be rebuilding them frag_index = self.node.get('index', self.job.get('frag_index')) if frag_index is None: # replication jobs will not have a frag_index key; # reconstructor jobs with only tombstones will have a # frag_index key explicitly set to the value of None - in both # cases on the wire we write the empty string which # ssync_receiver will translate to None frag_index = '' self.connection.putheader('X-Backend-Ssync-Frag-Index', frag_index) # a revert job to a handoff will not have a node index self.connection.putheader('X-Backend-Ssync-Node-Index', self.node.get('index', '')) self.connection.endheaders() with exceptions.MessageTimeout(self.daemon.node_timeout, 'connect receive'): self.response = self.connection.getresponse() if self.response.status != http.HTTP_OK: err_msg = self.response.read()[:1024] raise exceptions.ReplicationException( 'Expected status %s; got %s (%s)' % (http.HTTP_OK, self.response.status, err_msg))
def connect(self): """ Establishes a connection and starts a REPLICATION request with the object server. """ with exceptions.MessageTimeout(self.daemon.conn_timeout, 'connect send'): self.connection = bufferedhttp.BufferedHTTPConnection( '%s:%s' % (self.node['ip'], self.node['port'])) self.connection.putrequest( 'REPLICATION', '/%s/%s' % (self.node['device'], self.job['partition'])) self.connection.putheader('Transfer-Encoding', 'chunked') self.connection.putheader(POLICY_INDEX, self.policy_idx) self.connection.endheaders() with exceptions.MessageTimeout(self.daemon.node_timeout, 'connect receive'): self.response = self.connection.getresponse() if self.response.status != http.HTTP_OK: raise exceptions.ReplicationException( 'Expected status %s; got %s' % (http.HTTP_OK, self.response.status))
def subreq_iter(): left = content_length while left > 0: with exceptions.MessageTimeout( self.app.client_timeout, 'updates content'): chunk = self.fp.read( min(left, self.app.network_chunk_size)) if not chunk: raise exceptions.ChunkReadError( 'Early termination for %s %s' % (method, path)) left -= len(chunk) yield chunk
def disconnect(self): """ Closes down the connection to the object server once done with the SSYNC request. """ if not self.connection: return try: with exceptions.MessageTimeout(self.daemon.node_timeout, 'disconnect'): self.connection.send('0\r\n\r\n') except (Exception, exceptions.Timeout): pass # We're okay with the above failing. self.connection.close()
def updates(self): """ Handles the sender-side of the UPDATES step of an SSYNC request. Full documentation of this can be found at :py:meth:`.Receiver.updates`. """ # First, send all our subrequests based on the send_map. with exceptions.MessageTimeout(self.daemon.node_timeout, 'updates start'): msg = ':UPDATES: START\r\n' self.connection.send('%x\r\n%s\r\n' % (len(msg), msg)) for object_hash, want in self.send_map.items(): object_hash = urllib.parse.unquote(object_hash) try: df = self.df_mgr.get_diskfile_from_hash( self.job['device'], self.job['partition'], object_hash, self.job['policy'], frag_index=self.job.get('frag_index')) except exceptions.DiskFileNotExist: continue url_path = urllib.parse.quote('/%s/%s/%s' % (df.account, df.container, df.obj)) try: df.open() if want.get('data'): # EC reconstructor may have passed a callback to build an # alternative diskfile - construct it using the metadata # from the data file only. df_alt = self.job.get('sync_diskfile_builder', lambda *args: df)( self.job, self.node, df.get_datafile_metadata()) self.send_put(url_path, df_alt) if want.get('meta') and df.data_timestamp != df.timestamp: self.send_post(url_path, df) except exceptions.DiskFileDeleted as err: if want.get('data'): self.send_delete(url_path, err.timestamp) except exceptions.DiskFileError: # DiskFileErrors are expected while opening the diskfile, # before any data is read and sent. Since there is no partial # state on the receiver it's ok to ignore this diskfile and # continue. The diskfile may however be deleted after a # successful ssync since it remains in the send_map. pass with exceptions.MessageTimeout(self.daemon.node_timeout, 'updates end'): msg = ':UPDATES: END\r\n' self.connection.send('%x\r\n%s\r\n' % (len(msg), msg)) # Now, read their response for any issues. while True: with exceptions.MessageTimeout(self.daemon.http_timeout, 'updates start wait'): line = self.readline() if not line: raise exceptions.ReplicationException('Early disconnect') line = line.strip() if line == ':UPDATES: START': break elif line: raise exceptions.ReplicationException( 'Unexpected response: %r' % line[:1024]) while True: with exceptions.MessageTimeout(self.daemon.http_timeout, 'updates line wait'): line = self.readline() if not line: raise exceptions.ReplicationException('Early disconnect') line = line.strip() if line == ':UPDATES: END': break elif line: raise exceptions.ReplicationException( 'Unexpected response: %r' % line[:1024])
def updates(self): """ Handles the UPDATES step of an SSYNC request. Receives a set of PUT and DELETE subrequests that will be routed to the object server itself for processing. These contain the information requested by the MISSING_CHECK step. The PUT and DELETE subrequests are formatted pretty much exactly like regular HTTP requests, excepting the HTTP version on the first request line. The process is generally: 1. Sender sends `:UPDATES: START` and begins sending the PUT and DELETE subrequests. 2. Receiver gets `:UPDATES: START` and begins routing the subrequests to the object server. 3. Sender sends `:UPDATES: END`. 4. Receiver gets `:UPDATES: END` and sends `:UPDATES: START` and `:UPDATES: END` (assuming no errors). 5. Sender gets `:UPDATES: START` and `:UPDATES: END`. If too many subrequests fail, as configured by replication_failure_threshold and replication_failure_ratio, the receiver will hang up the request early so as to not waste any more time. At step 4, the receiver will send back an error if there were any failures (that didn't cause a hangup due to the above thresholds) so the sender knows the whole was not entirely a success. This is so the sender knows if it can remove an out of place partition, for example. """ with exceptions.MessageTimeout(self.app.client_timeout, 'updates start'): line = self.fp.readline(self.app.network_chunk_size) if line.strip() != ':UPDATES: START': raise Exception('Looking for :UPDATES: START got %r' % line[:1024]) successes = 0 failures = 0 while True: with exceptions.MessageTimeout(self.app.client_timeout, 'updates line'): line = self.fp.readline(self.app.network_chunk_size) if not line or line.strip() == ':UPDATES: END': break # Read first line METHOD PATH of subrequest. method, path = line.strip().split(' ', 1) subreq = swob.Request.blank('/%s/%s%s' % (self.device, self.partition, path), environ={'REQUEST_METHOD': method}) # Read header lines. content_length = None replication_headers = [] while True: with exceptions.MessageTimeout(self.app.client_timeout): line = self.fp.readline(self.app.network_chunk_size) if not line: raise Exception('Got no headers for %s %s' % (method, path)) line = line.strip() if not line: break header, value = line.split(':', 1) header = header.strip().lower() value = value.strip() subreq.headers[header] = value if header != 'etag': # make sure ssync doesn't cause 'Etag' to be added to # obj metadata in addition to 'ETag' which object server # sets (note capitalization) replication_headers.append(header) if header == 'content-length': content_length = int(value) # Establish subrequest body, if needed. if method in ('DELETE', 'POST'): if content_length not in (None, 0): raise Exception('%s subrequest with content-length %s' % (method, path)) elif method == 'PUT': if content_length is None: raise Exception('No content-length sent for %s %s' % (method, path)) def subreq_iter(): left = content_length while left > 0: with exceptions.MessageTimeout(self.app.client_timeout, 'updates content'): chunk = self.fp.read( min(left, self.app.network_chunk_size)) if not chunk: raise exceptions.ChunkReadError( 'Early termination for %s %s' % (method, path)) left -= len(chunk) yield chunk subreq.environ['wsgi.input'] = utils.FileLikeIter( subreq_iter()) else: raise Exception('Invalid subrequest method %s' % method) subreq.headers['X-Backend-Storage-Policy-Index'] = int(self.policy) subreq.headers['X-Backend-Replication'] = 'True' if self.node_index is not None: # primary node should not 409 if it has a non-primary fragment subreq.headers['X-Backend-Ssync-Frag-Index'] = self.node_index if replication_headers: subreq.headers['X-Backend-Replication-Headers'] = \ ' '.join(replication_headers) # Route subrequest and translate response. resp = subreq.get_response(self.app) if http.is_success(resp.status_int) or \ resp.status_int == http.HTTP_NOT_FOUND: successes += 1 else: self.app.logger.warning( 'ssync subrequest failed with %s: %s %s' % (resp.status_int, method, subreq.path)) failures += 1 if failures >= self.app.replication_failure_threshold and ( not successes or float(failures) / successes > self.app.replication_failure_ratio): raise Exception('Too many %d failures to %d successes' % (failures, successes)) # The subreq may have failed, but we want to read the rest of the # body from the remote side so we can continue on with the next # subreq. for junk in subreq.environ['wsgi.input']: pass if failures: raise swob.HTTPInternalServerError( 'ERROR: With :UPDATES: %d failures to %d successes' % (failures, successes)) yield ':UPDATES: START\r\n' yield ':UPDATES: END\r\n'
def missing_check(self): """ Handles the receiver-side of the MISSING_CHECK step of a SSYNC request. Receives a list of hashes and timestamps of object information the sender can provide and responds with a list of hashes desired, either because they're missing or have an older timestamp locally. The process is generally: 1. Sender sends `:MISSING_CHECK: START` and begins sending `hash timestamp` lines. 2. Receiver gets `:MISSING_CHECK: START` and begins reading the `hash timestamp` lines, collecting the hashes of those it desires. 3. Sender sends `:MISSING_CHECK: END`. 4. Receiver gets `:MISSING_CHECK: END`, responds with `:MISSING_CHECK: START`, followed by the list of <wanted_hash> specifiers it collected as being wanted (one per line), `:MISSING_CHECK: END`, and flushes any buffers. Each <wanted_hash> specifier has the form <hash>[ <parts>] where <parts> is a string containing characters 'd' and/or 'm' indicating that only data or meta part of object respectively is required to be sync'd. 5. Sender gets `:MISSING_CHECK: START` and reads the list of hashes desired by the receiver until reading `:MISSING_CHECK: END`. The collection and then response is so the sender doesn't have to read while it writes to ensure network buffers don't fill up and block everything. """ with exceptions.MessageTimeout(self.app.client_timeout, 'missing_check start'): line = self.fp.readline(self.app.network_chunk_size) if line.strip() != ':MISSING_CHECK: START': raise Exception('Looking for :MISSING_CHECK: START got %r' % line[:1024]) object_hashes = [] while True: with exceptions.MessageTimeout(self.app.client_timeout, 'missing_check line'): line = self.fp.readline(self.app.network_chunk_size) if not line or line.strip() == ':MISSING_CHECK: END': break want = self._check_missing(line) if want: object_hashes.append(want) yield ':MISSING_CHECK: START\r\n' if object_hashes: yield '\r\n'.join(object_hashes) yield '\r\n' yield ':MISSING_CHECK: END\r\n'
def missing_check(self): """ Handles the receiver-side of the MISSING_CHECK step of a SSYNC request. Receives a list of hashes and timestamps of object information the sender can provide and responds with a list of hashes desired, either because they're missing or have an older timestamp locally. The process is generally: 1. Sender sends `:MISSING_CHECK: START` and begins sending `hash timestamp` lines. 2. Receiver gets `:MISSING_CHECK: START` and begins reading the `hash timestamp` lines, collecting the hashes of those it desires. 3. Sender sends `:MISSING_CHECK: END`. 4. Receiver gets `:MISSING_CHECK: END`, responds with `:MISSING_CHECK: START`, followed by the list of hashes it collected as being wanted (one per line), `:MISSING_CHECK: END`, and flushes any buffers. 5. Sender gets `:MISSING_CHECK: START` and reads the list of hashes desired by the receiver until reading `:MISSING_CHECK: END`. The collection and then response is so the sender doesn't have to read while it writes to ensure network buffers don't fill up and block everything. """ with exceptions.MessageTimeout(self.app.client_timeout, 'missing_check start'): line = self.fp.readline(self.app.network_chunk_size) if line.strip() != ':MISSING_CHECK: START': raise Exception('Looking for :MISSING_CHECK: START got %r' % line[:1024]) object_hashes = [] while True: with exceptions.MessageTimeout(self.app.client_timeout, 'missing_check line'): line = self.fp.readline(self.app.network_chunk_size) if not line or line.strip() == ':MISSING_CHECK: END': break parts = line.split() object_hash, timestamp = [urllib.unquote(v) for v in parts[:2]] want = False try: df = self.diskfile_mgr.get_diskfile_from_hash( self.device, self.partition, object_hash, self.policy, frag_index=self.frag_index) except exceptions.DiskFileNotExist: want = True else: try: df.open() except exceptions.DiskFileDeleted as err: want = err.timestamp < timestamp except exceptions.DiskFileError as err: want = True else: want = df.timestamp < timestamp if want: object_hashes.append(object_hash) yield ':MISSING_CHECK: START\r\n' if object_hashes: yield '\r\n'.join(object_hashes) yield '\r\n' yield ':MISSING_CHECK: END\r\n'
def updates(self): """ Handles the sender-side of the UPDATES step of an SSYNC request. Full documentation of this can be found at :py:meth:`.Receiver.updates`. """ # First, send all our subrequests based on the send_list. with exceptions.MessageTimeout(self.daemon.node_timeout, 'updates start'): msg = ':UPDATES: START\r\n' self.connection.send('%x\r\n%s\r\n' % (len(msg), msg)) for object_hash in self.send_list: try: df = self.df_mgr.get_diskfile_from_hash( self.job['device'], self.job['partition'], object_hash, self.job['policy'], frag_index=self.job.get('frag_index')) except exceptions.DiskFileNotExist: continue url_path = urllib.quote('/%s/%s/%s' % (df.account, df.container, df.obj)) try: df.open() # EC reconstructor may have passed a callback to build # an alternative diskfile... df = self.job.get('sync_diskfile_builder', lambda *args: df)(self.job, self.node, df.get_metadata()) except exceptions.DiskFileDeleted as err: self.send_delete(url_path, err.timestamp) except exceptions.DiskFileError: pass else: self.send_put(url_path, df) with exceptions.MessageTimeout(self.daemon.node_timeout, 'updates end'): msg = ':UPDATES: END\r\n' self.connection.send('%x\r\n%s\r\n' % (len(msg), msg)) # Now, read their response for any issues. while True: with exceptions.MessageTimeout(self.daemon.http_timeout, 'updates start wait'): line = self.readline() if not line: raise exceptions.ReplicationException('Early disconnect') line = line.strip() if line == ':UPDATES: START': break elif line: raise exceptions.ReplicationException( 'Unexpected response: %r' % line[:1024]) while True: with exceptions.MessageTimeout(self.daemon.http_timeout, 'updates line wait'): line = self.readline() if not line: raise exceptions.ReplicationException('Early disconnect') line = line.strip() if line == ':UPDATES: END': break elif line: raise exceptions.ReplicationException( 'Unexpected response: %r' % line[:1024])
def connect(self): exc = exceptions.MessageTimeout(1, 'test connect') # Cancels Eventlet's raising of this since we're about to do it. exc.cancel() raise exc
def missing_check(self, connection, response): """ Handles the sender-side of the MISSING_CHECK step of a SSYNC request. Full documentation of this can be found at :py:meth:`.Receiver.missing_check`. """ available_map = {} send_map = {} # First, send our list. with exceptions.MessageTimeout(self.daemon.node_timeout, 'missing_check start'): msg = b':MISSING_CHECK: START\r\n' connection.send(b'%x\r\n%s\r\n' % (len(msg), msg)) hash_gen = self.df_mgr.yield_hashes( self.job['device'], self.job['partition'], self.job['policy'], self.suffixes, frag_index=self.job.get('frag_index')) if self.remote_check_objs is not None: hash_gen = six.moves.filter( lambda objhash_timestamps: objhash_timestamps[0] in self. remote_check_objs, hash_gen) for object_hash, timestamps in hash_gen: available_map[object_hash] = timestamps with exceptions.MessageTimeout(self.daemon.node_timeout, 'missing_check send line'): msg = b'%s\r\n' % encode_missing(object_hash, **timestamps) connection.send(b'%x\r\n%s\r\n' % (len(msg), msg)) with exceptions.MessageTimeout(self.daemon.node_timeout, 'missing_check end'): msg = b':MISSING_CHECK: END\r\n' connection.send(b'%x\r\n%s\r\n' % (len(msg), msg)) # Now, retrieve the list of what they want. while True: with exceptions.MessageTimeout(self.daemon.http_timeout, 'missing_check start wait'): line = response.readline(size=self.daemon.network_chunk_size) if not line: raise exceptions.ReplicationException('Early disconnect') line = line.strip() if line == b':MISSING_CHECK: START': break elif line: if not six.PY2: try: line = line.decode('ascii') except UnicodeDecodeError: pass raise exceptions.ReplicationException( 'Unexpected response: %r' % line[:1024]) while True: with exceptions.MessageTimeout(self.daemon.http_timeout, 'missing_check line wait'): line = response.readline(size=self.daemon.network_chunk_size) if not line: raise exceptions.ReplicationException('Early disconnect') line = line.strip() if line == b':MISSING_CHECK: END': break parts = line.decode('ascii').split() if parts: send_map[parts[0]] = decode_wanted(parts[1:]) return available_map, send_map