@Override
        protected void run(ChannelHandlerContext ctx) {

            final long lastPingIdleTime = AbstractKeepAliveHandler.this.lastPingIdleTime;
            final long nextDelay;
            nextDelay = pingIdleTimeNanos - (System.nanoTime() - lastPingIdleTime);
            if (nextDelay <= 0) {
                // PING is idle - set a new timeout and notify the callback.
                pingIdleTimeout = executor().schedule(this, pingIdleTimeNanos, TimeUnit.NANOSECONDS);

                final boolean isFirst = firstPingIdleEvent;
                firstPingIdleEvent = false;
                try {
                    if (pingIdleTimeNanos > 0 && isFirst) {
                        pingState = PingState.PING_SCHEDULED;
                        writePing(ctx).addListener(pingWriteListener);
                    }
                } catch (Exception e) {
                    if (!warn) {
                        logger.warn("An error occurred while notifying a ping idle event", e);
                        warn = true;
                    }
                }
            } else {
                // A PING was sent or received within the ping timeout
                // - set a new timeout with shorter delay.
                pingIdleTimeout = 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