예제 #1
0
 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))
예제 #2
0
 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))
예제 #3
0
 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
예제 #4
0
 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()
예제 #5
0
    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])
예제 #6
0
    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'
예제 #7
0
    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'
예제 #8
0
    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'
예제 #9
0
    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])
예제 #10
0
 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
예제 #11
0
    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