@Override
        protected void run(ChannelHandlerContext ctx) {
            if (pingState == PingState.SHUTDOWN) {
                return;
            }

            final long lastConnectionIdleTime = AbstractKeepAliveHandler.this.lastConnectionIdleTime;
            final long nextDelay;
            nextDelay = connectionIdleTimeNanos - (System.nanoTime() - lastConnectionIdleTime);
            if (nextDelay <= 0) {
                // Both reader and writer are idle - set a new timeout and
                // notify the callback.
                connectionIdleTimeout = executor().schedule(this, connectionIdleTimeNanos,
                                                            TimeUnit.NANOSECONDS);
                try {
                    if (!hasRequestsInProgress(ctx)) {
                        pingState = PingState.SHUTDOWN;
                        logger.debug("{} Closing an idle {} connection", ctx.channel(), name);
                        ctx.channel().close();
                    }
                } catch (Exception e) {
                    if (!warn) {
                        logger.warn("An error occurred while notifying an all idle event", e);
                        warn = true;
                    }
                }
            } else {
                // Either read or write occurred before the connection idle timeout - set a new
                // timeout with shorter delay.
                connectionIdleTimeout = executor().schedule(this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
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