/**
   * Called to signal async read isReady() has returned false. This indicates that there is no
   * content available to be consumed and that once the channel enteres the ASYNC_WAIT state it will
   * register for read interest by calling {@link HttpChannel#asyncReadFillInterested()} either from
   * this method or from a subsequent call to {@link #unhandle()}.
   */
  public void onReadUnready() {
    boolean interested = false;
    try (Locker.Lock lock = _locker.lock()) {
      if (DEBUG) LOG.debug("onReadUnready {}", toStringLocked());

      // We were already unready, this is not a state change, so do nothing
      if (!_asyncReadUnready) {
        _asyncReadUnready = true;
        _asyncReadPossible = false; // Assumes this has been checked in isReady() with lock held
        if (_state == State.ASYNC_WAIT) interested = true;
      }
    }

    if (interested) _channel.asyncReadFillInterested();
  }
  /**
   * Signal that the HttpConnection has finished handling the request. For blocking connectors,this
   * call may block if the request has been suspended (startAsync called).
   *
   * @return next actions be handled again (eg because of a resume that happened before unhandle was
   *     called)
   */
  protected Action unhandle() {
    Action action;
    AsyncContextEvent schedule_event = null;
    boolean read_interested = false;

    try (Locker.Lock lock = _locker.lock()) {
      if (DEBUG) LOG.debug("unhandle {}", toStringLocked());

      switch (_state) {
        case COMPLETING:
        case COMPLETED:
          return Action.TERMINATED;

        case THROWN:
          _state = State.DISPATCHED;
          return Action.ERROR_DISPATCH;

        case DISPATCHED:
        case ASYNC_IO:
          break;

        default:
          throw new IllegalStateException(this.getStatusStringLocked());
      }

      if (_async != null) {
        _initial = false;
        switch (_async) {
          case COMPLETE:
            _state = State.COMPLETING;
            _async = null;
            action = Action.COMPLETE;
            break;

          case DISPATCH:
            _state = State.DISPATCHED;
            _async = null;
            action = Action.ASYNC_DISPATCH;
            break;

          case STARTED:
            if (_asyncReadUnready && _asyncReadPossible) {
              _state = State.ASYNC_IO;
              _asyncReadUnready = false;
              action = Action.READ_CALLBACK;
            } else if (_asyncWrite) // TODO refactor same as read
            {
              _asyncWrite = false;
              _state = State.ASYNC_IO;
              action = Action.WRITE_CALLBACK;
            } else {
              schedule_event = _event;
              read_interested = _asyncReadUnready;
              _state = State.ASYNC_WAIT;
              action = Action.WAIT;
            }
            break;

          case EXPIRING:
            // onTimeout callbacks still being called, so just WAIT
            _state = State.ASYNC_WAIT;
            action = Action.WAIT;
            break;

          case EXPIRED:
            // onTimeout handling is complete, but did not dispatch as
            // we were handling.  So do the error dispatch here
            _state = State.DISPATCHED;
            _async = null;
            action = Action.ERROR_DISPATCH;
            break;

          case ERRORED:
            _state = State.DISPATCHED;
            _async = null;
            action = Action.ERROR_DISPATCH;
            break;

          default:
            throw new IllegalStateException(this.getStatusStringLocked());
        }
      } else {
        _state = State.COMPLETING;
        action = Action.COMPLETE;
      }
    }

    if (schedule_event != null) scheduleTimeout(schedule_event);
    if (read_interested) _channel.asyncReadFillInterested();
    return action;
  }