docs: def _read2(self, length=None, use_compression=None, project=None, **kwargs):
          '''
          :param length: Maximum number of bytes to be read
          :type length: integer
          :param project: project to use as context for this download (may affect
              which billing account is billed for this download). If specified,
              must be a project in which this file exists. If not specified, the
              project ID specified in the handler is used for the download, IF it
              contains this file. If set to DXFile.NO_PROJECT_HINT, no project ID
              is supplied for the download, even if the handler specifies a
              project ID.
          :type project: str or None
          :rtype: string
          :raises: :exc:`~dxpy.exceptions.ResourceNotFound` if *project* is supplied
             and it does not contain this file
  
          Returns the next *length* bytes, or all the bytes until the end of file
          (if no *length* is given or there are fewer than *length* bytes left in
          the file).
  
          .. note:: After the first call to read(), the project arg and
             passthrough kwargs are not respected while using the same response
             iterator (i.e. until next seek).
  
          '''
          if self._file_length == None:
              desc = self.describe(**kwargs)
              if desc["state"] != "closed":
                  raise DXFileError("Cannot read from file until it is in the closed state")
              self._file_length = int(desc["size"])
  
          # If running on a worker, wait for the first file download chunk
          # to come back before issuing any more requests. This ensures
          # that all subsequent requests can take advantage of caching,
          # rather than having all of the first DXFILE_HTTP_THREADS
          # requests simultaneously hit a cold cache. Enforce a minimum
          # size for this heuristic so we don't incur the overhead for
          # tiny files (which wouldn't contribute as much to the load
          # anyway).
          get_first_chunk_sequentially = (self._file_length > 128 * 1024 and self._pos == 0 and dxpy.JOB_ID)
  
          if self._pos == self._file_length:
              return b""
  
          if length == None or length > self._file_length - self._pos:
              length = self._file_length - self._pos
  
          buf = self._read_buf
          buf_remaining_bytes = dxpy.utils.string_buffer_length(buf) - buf.tell()
          if length <= buf_remaining_bytes:
              self._pos += length
              return buf.read(length)
          else:
              orig_buf_pos = buf.tell()
              orig_file_pos = self._pos
              buf.seek(0, os.SEEK_END)
              self._pos += buf_remaining_bytes
              while self._pos < orig_file_pos + length:
                  remaining_len = orig_file_pos + length - self._pos
  
                  if self._response_iterator is None:
                      self._request_iterator = self._generate_read_requests(
                          start_pos=self._pos, project=project, **kwargs)
  
                  content = self._next_response_content(get_first_chunk_sequentially=get_first_chunk_sequentially)
  
                  if len(content) < remaining_len:
                      buf.write(content)
                      self._pos += len(content)
                  else: # response goes beyond requested length
                      buf.write(content[:remaining_len])
                      self._pos += remaining_len
                      self._read_buf = BytesIO()
                      self._read_buf.write(content[remaining_len:])
                      self._read_buf.seek(0)
              buf.seek(orig_buf_pos)
              return buf.read()