def check_version(self, timeout=2, strict=False): """Attempt to guess the broker version. Note: This is a blocking call. Returns: version tuple, i.e. (0, 10), (0, 9), (0, 8, 2), ... """ # Monkeypatch some connection configurations to avoid timeouts override_config = { 'request_timeout_ms': timeout * 1000, 'max_in_flight_requests_per_connection': 5 } stashed = {} for key in override_config: stashed[key] = self.config[key] self.config[key] = override_config[key] # kafka kills the connection when it doesnt recognize an API request # so we can send a test request and then follow immediately with a # vanilla MetadataRequest. If the server did not recognize the first # request, both will be failed with a ConnectionError that wraps # socket.error (32, 54, or 104) from .protocol.admin import ApiVersionRequest, ListGroupsRequest from .protocol.commit import OffsetFetchRequest, GroupCoordinatorRequest from .protocol.metadata import MetadataRequest # Socket errors are logged as exceptions and can alarm users. Mute them from logging import Filter class ConnFilter(Filter): def filter(self, record): if record.funcName == 'check_version': return True return False log_filter = ConnFilter() log.addFilter(log_filter) test_cases = [ ((0, 10), ApiVersionRequest[0]()), ((0, 9), ListGroupsRequest[0]()), ((0, 8, 2), GroupCoordinatorRequest[0]('kafka-python-default-group')), ((0, 8, 1), OffsetFetchRequest[0]('kafka-python-default-group', [])), ((0, 8, 0), MetadataRequest[0]([])), ] def connect(): self.connect() if self.connected(): return timeout_at = time.time() + timeout while time.time() < timeout_at and self.connecting(): if self.connect() is ConnectionStates.CONNECTED: return time.sleep(0.05) raise Errors.NodeNotReadyError() for version, request in test_cases: connect() f = self.send(request) # HACK: sleeping to wait for socket to send bytes time.sleep(0.1) # when broker receives an unrecognized request API # it abruptly closes our socket. # so we attempt to send a second request immediately # that we believe it will definitely recognize (metadata) # the attempt to write to a disconnected socket should # immediately fail and allow us to infer that the prior # request was unrecognized mr = self.send(MetadataRequest[0]([])) if self._sock: self._sock.setblocking(True) while not (f.is_done and mr.is_done): self.recv() if self._sock: self._sock.setblocking(False) if f.succeeded(): log.info('Broker version identifed as %s', '.'.join(map(str, version))) log.info('Set configuration api_version=%s to skip auto' ' check_version requests on startup', version) break # Only enable strict checking to verify that we understand failure # modes. For most users, the fact that the request failed should be # enough to rule out a particular broker version. if strict: # If the socket flush hack did not work (which should force the # connection to close and fail all pending requests), then we # get a basic Request Timeout. This is not ideal, but we'll deal if isinstance(f.exception, Errors.RequestTimedOutError): pass # 0.9 brokers do not close the socket on unrecognized api # requests (bug...). In this case we expect to see a correlation # id mismatch elif (isinstance(f.exception, Errors.CorrelationIdError) and version == (0, 10)): pass elif six.PY2: assert isinstance(f.exception.args[0], socket.error) assert f.exception.args[0].errno in (32, 54, 104) else: assert isinstance(f.exception.args[0], ConnectionError) log.info("Broker is not v%s -- it did not recognize %s", version, request.__class__.__name__) else: raise Errors.UnrecognizedBrokerVersion() log.removeFilter(log_filter) for key in stashed: self.config[key] = stashed[key] return version
def check_version(self, timeout=2, strict=False): """Attempt to guess the broker version. This is a blocking call.""" # Monkeypatch the connection request timeout # Generally this timeout should not get triggered # but in case it does, we want it to be reasonably short stashed_request_timeout_ms = self.config['request_timeout_ms'] self.config['request_timeout_ms'] = timeout * 1000 # kafka kills the connection when it doesnt recognize an API request # so we can send a test request and then follow immediately with a # vanilla MetadataRequest. If the server did not recognize the first # request, both will be failed with a ConnectionError that wraps # socket.error (32, 54, or 104) from .protocol.admin import ListGroupsRequest from .protocol.commit import OffsetFetchRequest, GroupCoordinatorRequest from .protocol.metadata import MetadataRequest # Socket errors are logged as exceptions and can alarm users. Mute them from logging import Filter class ConnFilter(Filter): def filter(self, record): if record.funcName in ('recv', 'send'): return False return True log_filter = ConnFilter() log.addFilter(log_filter) test_cases = [ ('0.9', ListGroupsRequest[0]()), ('0.8.2', GroupCoordinatorRequest[0]('kafka-python-default-group')), ('0.8.1', OffsetFetchRequest[0]('kafka-python-default-group', [])), ('0.8.0', MetadataRequest[0]([])), ] def connect(): self.connect() if self.connected(): return timeout_at = time.time() + timeout while time.time() < timeout_at and self.connecting(): if self.connect() is ConnectionStates.CONNECTED: return time.sleep(0.05) raise Errors.NodeNotReadyError() for version, request in test_cases: connect() f = self.send(request) # HACK: sleeping to wait for socket to send bytes time.sleep(0.1) # when broker receives an unrecognized request API # it abruptly closes our socket. # so we attempt to send a second request immediately # that we believe it will definitely recognize (metadata) # the attempt to write to a disconnected socket should # immediately fail and allow us to infer that the prior # request was unrecognized self.send(MetadataRequest[0]([])) if self._sock: self._sock.setblocking(True) while not f.is_done: self.recv() if self._sock: self._sock.setblocking(False) if f.succeeded(): log.info('Broker version identifed as %s', version) log.info( "Set configuration api_version='%s' to skip auto" " check_version requests on startup", version) break # Only enable strict checking to verify that we understand failure # modes. For most users, the fact that the request failed should be # enough to rule out a particular broker version. if strict: # If the socket flush hack did not work (which should force the # connection to close and fail all pending requests), then we # get a basic Request Timeout. This is not ideal, but we'll deal if isinstance(f.exception, Errors.RequestTimedOutError): pass elif six.PY2: assert isinstance(f.exception.args[0], socket.error) assert f.exception.args[0].errno in (32, 54, 104) else: assert isinstance(f.exception.args[0], ConnectionError) log.info("Broker is not v%s -- it did not recognize %s", version, request.__class__.__name__) else: raise Errors.UnrecognizedBrokerVersion() log.removeFilter(log_filter) self.config['request_timeout_ms'] = stashed_request_timeout_ms return version