docs: @Override
      public long read(long numBytes, int timeout) throws IOException {
          if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) {
              Tr.entry(tc, "read, numbytes=" + numBytes + ", vc=" + getVCHash());
          }
  
          // Access the read interface of the device side channel.
          if (deviceReadContext == null) {
              deviceReadContext = getConnLink().getDeviceReadInterface();
          }
  
          // Handle timing out of former read request.
          if (timeout == IMMED_TIMEOUT || timeout == ABORT_TIMEOUT) {
              if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                  Tr.debug(tc, "Requested to timeout former request.  Calling device side.");
              }
              deviceReadContext.read(numBytes, timeout);
              if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) {
                  Tr.exit(tc, "read");
              }
              return 0L;
          }
  
          // Reset some object variables.
          this.decryptedNetBufferReleaseRequired = false;
          this.callerRequiredAllocation = false;
  
          // Look for errors in the request.
          IOException exceptionInRequest = checkRequest(numBytes, false);
          if (exceptionInRequest != null) {
              // Found an error.
              throw exceptionInRequest;
          }
  
          // Track the number of bytes requested.
          this.bytesRequested = numBytes;
  
          // save the starting positions of the buffers to make a proper return value later
          saveDecryptedPositions();
  
          // Handle any left over data from a the previous read request.
          this.bytesProduced = readUnconsumedDecData();
          if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
              Tr.debug(tc, "Decrypted data left over from previous request: " + bytesProduced);
          }
          // Was data read to satisfy the request or was the request to get all data available?
          if ((bytesRequested > bytesProduced) || (bytesRequested == 0L)) {
              // Left over data didn't exist or was not enough to satisfy the request.
              long devBytesRead = 0;
              int loopCount = 0;
              long encryptedBytesAvailable = 0;
              long bufferSize = 0;
  
              // If bytesProduced > 0, then some date was left over in unconsumedDecData but it
              // wasn't enough. The data is currently between pos and lim, but now we need to
              // treat it like data that was already decrypted by the JSSE, pushing the data
              // behind the position.
              if (bytesProduced > 0L) {
                  SSLUtils.positionToLimit(decryptedNetBuffers);
                  // While we're at it, maximize the room for future reads.
                  SSLUtils.limitToCapacity(decryptedNetBuffers);
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "Adjusted left over decNetBuffers: "
                                   + SSLUtils.getBufferTraceInfo(decryptedNetBuffers));
                  }
              }
  
              // Calculate how much encrypted data is currently available if netbuffers already exists.
              if (null != this.netBuffer) {
                  encryptedBytesAvailable = this.netBuffer.remaining();
              }
              // Adjust the buffer size if the number of bytes request was zero (read all).
              if (bytesRequested != 0L) {
                  bufferSize = bytesRequested - bytesProduced;
              }
              // Allocate/reuse the buffer to be read into. Data remaining from a previous,
              // unconsumed read, will be put in these buffers before return. Note the size
              // provided is a guess. bytesToRead is decNet data (decrypted). This buffer
              // is for net data (encrypted).
              getNetworkBuffer(bufferSize);
  
              // Keep reading until enough data has been read to satisfy the request.
              while ((bytesProduced < bytesRequested) || (bytesRequested == 0L)) {
                  // Adjust counter to know when first pass of loop took place.
                  loopCount++;
  
                  // Bypass a new read if data is available and this is the first loop iteration.
                  // Future loop will require a read since as the JSSE's UNDERFLOW will indicate.
                  if ((encryptedBytesAvailable > 0) && (loopCount == 1)) {
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "No read needed.  Encrypted bytes already available: "
                                       + encryptedBytesAvailable);
                      }
                  } else {
                      // Shift limit to capacity for all buffers to maximize room to read,
                      // reducing allocations.
                      this.netBuffer.limit(this.netBuffer.capacity());
                      long deviceReadRequestSize = 0;
                      if (bytesRequested != 0L && bytesRequested > bytesProduced) {
                          // Attempt to do as much reading as possible up front.
                          deviceReadRequestSize = bytesRequested - bytesProduced - encryptedBytesAvailable;
                          // PK32916 - 0 bytes is not valid in this case so protect against that
                          // boundary condition
                          if (0L >= deviceReadRequestSize) {
                              deviceReadRequestSize = 1;
                          }
                      } else if (bytesRequested == 0L) {
                          deviceReadRequestSize = 0L;
                      } else {
                          // Remember bytesRequested are decrypted bytes.
                          // devBytesRead are encrypted bytes. This case can and
                          // will happen so ensure some reading gets done. Set to 1.
                          deviceReadRequestSize = 1;
                      }
                      if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) {
                          Tr.event(tc, "do sync read of : " + deviceReadRequestSize
                                       + " bytes into netBuffer..." + SSLUtils.getBufferTraceInfo(this.netBuffer));
                      }
                      try {
                          devBytesRead += deviceReadContext.read(deviceReadRequestSize, timeout);
                          if (deviceReadRequestSize == 0L && devBytesRead == 0L) {
                              // A request was made to read all available, and nothing was.
                              this.bytesProduced = 0L;
  
                              // Reset the buffers to contain previously read data between pos and lim.
                              this.netBuffer.limit(this.netBuffer.position());
                              this.netBuffer.position(netBufferMark);
                              break;
                          }
                      } catch (IOException e) {
                          // no FFDC required
                          // Protect future reads from thinking data was read.
                          // Reset the buffers to contain previously read data between pos and lim.
                          this.netBuffer.limit(this.netBuffer.position());
                          this.netBuffer.position(netBufferMark);
                          if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                              Tr.debug(tc, "Reset buffers after read error: vc=" + getVCHash()
                                           + ", netBuffer: " + SSLUtils.getBufferTraceInfo(this.netBuffer));
                          }
                          throw e;
                      }
                      if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) {
                          Tr.event(tc, "read bytes (total): " + devBytesRead);
                      }
                  }
  
                  // Prepare the input buffer for calling unwrap.
                  // Set the position to the mark, noting the beginning of the data just read.
                  this.netBuffer.limit(this.netBuffer.position());
                  this.netBuffer.position(netBufferMark);
  
                  // decrypt the resulting message.
                  Exception exception = decryptMessage(false);
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, bytesProduced + " bytes produced of "
                                   + bytesRequested + " bytes requested");
                  }
  
                  // Calculate how much encrypted data is still available, for use in future iterations.
                  encryptedBytesAvailable = this.netBuffer.remaining();
  
                  if (exception == null) {
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "Decryption succeeded.");
                      }
                      // Check if more reading is necessary.
                      if (0L == this.bytesRequested) {
                          break;
                      }
                      if (bytesProduced < bytesRequested) {
                          getNetworkBuffer(bytesRequested - bytesProduced);
                      }
                  }
  
                  else if (exception instanceof ReadNeededInternalException) {
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "More data needs to be read, loop to another read");
                      }
                      if (0L == this.bytesRequested) {
                          break;
                      }
                      getNetworkBuffer(this.bytesRequested - this.bytesProduced);
                      continue;
                  }
  
                  else if (exception instanceof SessionClosedException) {
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "SSL Session has been closed.");
                      }
                      // 238579 - inform channel above
                      throw new IOException("SSL connection was closed by peer");
                  }
  
                  else {
                      FFDCFilter.processException(exception, getClass().getName(), "118", this);
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "Decryption unsuccessful, exception resulted: " + exception);
                      }
                      IOException exc = new IOException("Unable to decrypt message");
                      exc.initCause(exception);
                      throw exc;
                  }
              } // end while
          }
          // A valid amount of data has been produced.
          if (this.bytesProduced > 0L) {
              prepareDataForNextChannel();
              // now figure out how much we're actually sending to the caller
              WsByteBuffer[] buffers = getBuffers();
              long count = 0L;
              for (int i = 0; i < buffers.length && count < this.bytesProduced; i++) {
                  count += buffers[i].limit() - decryptedNetPosInfo[i];
              }
              this.bytesProduced = count;
          }
  
          if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) {
              Tr.exit(tc, "read: " + this.bytesProduced);
          }
          return this.bytesProduced;
      }