class SchemaIndexCacheVisitorImpl implements SchemaIndexCacheVisitor {
    /** Is extra index rebuild logging enabled. */
    private final boolean collectStat = getBoolean(IGNITE_ENABLE_EXTRA_INDEX_REBUILD_LOGGING, false);

    /** Cache context. */
    private final GridCacheContext cctx;

    /** Cancellation token. */
    private final IndexRebuildCancelToken cancelTok;

    /** Future for create/rebuild index. */
    protected final GridFutureAdapter<Void> buildIdxFut;

    /** Logger. */
    protected final IgniteLogger log;

    /**
     * Constructor.
     *
     * @param cctx Cache context.
     * @param cancelTok Cancellation token.
     * @param buildIdxFut Future for create/rebuild index.
     */
    public SchemaIndexCacheVisitorImpl(
        GridCacheContext<?, ?> cctx,
        IndexRebuildCancelToken cancelTok,
        GridFutureAdapter<Void> buildIdxFut
    ) {
        assert nonNull(cctx);
        assert nonNull(buildIdxFut);

        if (cctx.isNear())
            cctx = ((GridNearCacheAdapter)cctx.cache()).dht().context();

        this.cctx = cctx;
        this.buildIdxFut = buildIdxFut;

        this.cancelTok = cancelTok;

        log = cctx.kernalContext().log(getClass());
    }

    /** {@inheritDoc} */
    @Override public void visit(SchemaIndexCacheVisitorClosure clo) {
        assert nonNull(clo);

        List<GridDhtLocalPartition> locParts = cctx.topology().localPartitions();

        if (locParts.isEmpty()) {
            buildIdxFut.onDone();

            return;
        }

        cctx.cache().metrics0().addIndexBuildPartitionsLeftCount(locParts.size());
        cctx.cache().metrics0().resetIndexRebuildKeyProcessed();

        beforeExecute();

        AtomicInteger partsCnt = new AtomicInteger(locParts.size());

        AtomicBoolean stop = new AtomicBoolean();

        // To avoid a race between clearing pageMemory (on a cache stop ex. deactivation)
        // and rebuilding indexes, which can lead to a fail of the node.
        SchemaIndexCacheCompoundFuture buildIdxCompoundFut = new SchemaIndexCacheCompoundFuture();

        for (GridDhtLocalPartition locPart : locParts) {
            GridWorkerFuture<SchemaIndexCacheStat> workerFut = new GridWorkerFuture<>();

            GridWorker worker =
                new SchemaIndexCachePartitionWorker(cctx, locPart, stop, cancelTok, clo, workerFut, partsCnt);

            workerFut.setWorker(worker);
            buildIdxCompoundFut.add(workerFut);

            cctx.kernalContext().pools().buildIndexExecutorService().execute(worker);
        }

        buildIdxCompoundFut.listen(() -> {
            Throwable err = buildIdxCompoundFut.error();

            if (isNull(err) && collectStat && log.isInfoEnabled()) {
                try {
                    SchemaIndexCacheStat resStat = new SchemaIndexCacheStat();

                    buildIdxCompoundFut.futures().stream()
                        .map(IgniteInternalFuture::result)
                        .filter(Objects::nonNull)
                        .forEach(resStat::accumulate);

                    log.info(indexStatStr(resStat));
                }
                catch (Exception e) {
                    log.error("Error when trying to print index build/rebuild statistics [cacheName=" +
                        cctx.cache().name() + ", grpName=" + cctx.group().name() + "]", e);
                }
            }

            buildIdxFut.onDone(err);
        });

        buildIdxCompoundFut.markInitialized();
    }

    /**
     * Prints index cache stats to log.
     *
     * @param stat Index cache stats.
     * @throws IgniteCheckedException if failed to get index size.
     */
    private String indexStatStr(SchemaIndexCacheStat stat) throws IgniteCheckedException {<FILL_FUNCTION_BODY>}

    /**
     * This method is called before creating or rebuilding indexes.
     * Used only for test.
     */
    protected void beforeExecute(){
        //no-op
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return S.toString(SchemaIndexCacheVisitorImpl.class, this);
    }
}

        SB res = new SB();

        res.a("Details for cache rebuilding [name=" + cctx.cache().name() + ", grpName=" + cctx.group().name() + ']');
        res.a(U.nl());
        res.a("   Scanned rows " + stat.scannedKeys() + ", visited types " + stat.typeNames());
        res.a(U.nl());

        IndexProcessor idxProc = cctx.kernalContext().indexProcessor();

        for (QueryTypeDescriptorImpl type : stat.types()) {
            res.a("        Type name=" + type.name());
            res.a(U.nl());

            String pk = QueryUtils.PRIMARY_KEY_INDEX;
            String tblName = type.tableName();

            res.a("            Index: name=" + pk + ", size=" + idxProc.index(new IndexName(
                cctx.cache().name(), type.schemaName(), tblName, pk)).unwrap(InlineIndex.class).totalCount());
            res.a(U.nl());

            for (GridQueryIndexDescriptor descriptor : type.indexes().values()) {
                long size = idxProc.index(new IndexName(
                    cctx.cache().name(), type.schemaName(), tblName, pk)).unwrap(InlineIndex.class).totalCount();

                res.a("            Index: name=" + descriptor.name() + ", size=" + size);
                res.a(U.nl());
            }
        }

        return res.toString();
    
<no_super_class>