@Override
        public void operationComplete(ChannelFuture future) throws Exception {
            if (future.isSuccess()) {
                logger.debug("{} PING write successful", channel);
                final EventLoop el = channel.eventLoop();
                shutdownFuture = el.schedule(shutdownRunnable, pingIdleTimeNanos, TimeUnit.NANOSECONDS);
                pingState = PingState.PENDING_PING_ACK;
                resetStopwatch();
            } else {
                // Mostly because the channel is already closed. So ignore and change state to IDLE.
                // If the channel is closed, we change state to SHUTDOWN on destroy.
                if (!future.isCancelled() && Exceptions.isExpected(future.cause())) {
                    logger.debug("{} PING write failed", channel, future.cause());
                }
                if (pingState != PingState.SHUTDOWN) {
                    pingState = PingState.IDLE;
                }
            }
        }
State changes from IDLE -> PING_SCHEDULED -> PENDING_PING_ACK -> IDLE and so on. When the
channel is inactive then the state changes to SHUTDOWN.
core/src/main/java/com/linecorp/armeria/internal/common/AbstractKeepAliveHandler.java