All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.xpack.esql.io.stream.PlanStreamOutput Maven / Gradle / Ivy

The newest version!
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */

package org.elasticsearch.xpack.esql.io.stream;

import org.elasticsearch.TransportVersion;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.compute.data.Block;
import org.elasticsearch.compute.data.BlockFactory;
import org.elasticsearch.compute.data.BooleanBigArrayBlock;
import org.elasticsearch.compute.data.DoubleBigArrayBlock;
import org.elasticsearch.compute.data.IntBigArrayBlock;
import org.elasticsearch.compute.data.LongBigArrayBlock;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xpack.esql.Column;
import org.elasticsearch.xpack.esql.core.plan.logical.LogicalPlan;
import org.elasticsearch.xpack.esql.io.stream.PlanNameRegistry.PlanWriter;
import org.elasticsearch.xpack.esql.plan.logical.join.Join;
import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan;
import org.elasticsearch.xpack.esql.session.EsqlConfiguration;

import java.io.IOException;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.function.Function;

/**
 * A customized stream output used to serialize ESQL physical plan fragments. Complements stream
 * output with methods that write plan nodes, Attributes, Expressions, etc.
 */
public final class PlanStreamOutput extends StreamOutput {

    /**
     * Cache of written blocks. We use an {@link IdentityHashMap} for this
     * because calculating the {@link Object#hashCode} of a {@link Block}
     * is slow. And so is {@link Object#equals}. So, instead we just use
     * object identity.
     */
    private final Map cachedBlocks = new IdentityHashMap<>();

    private final StreamOutput delegate;
    private final PlanNameRegistry registry;

    private final Function, String> nameSupplier;

    private int nextCachedBlock = 0;

    public PlanStreamOutput(StreamOutput delegate, PlanNameRegistry registry, @Nullable EsqlConfiguration configuration)
        throws IOException {
        this(delegate, registry, configuration, PlanNamedTypes::name);
    }

    public PlanStreamOutput(
        StreamOutput delegate,
        PlanNameRegistry registry,
        @Nullable EsqlConfiguration configuration,
        Function, String> nameSupplier
    ) throws IOException {
        this.delegate = delegate;
        this.registry = registry;
        this.nameSupplier = nameSupplier;
        if (configuration != null) {
            for (Map.Entry> table : configuration.tables().entrySet()) {
                for (Map.Entry column : table.getValue().entrySet()) {
                    cachedBlocks.put(column.getValue().values(), fromConfigKey(table.getKey(), column.getKey()));
                }
            }
        }
    }

    public void writeLogicalPlanNode(LogicalPlan logicalPlan) throws IOException {
        assert logicalPlan.children().size() <= 1 || (logicalPlan instanceof Join && logicalPlan.children().size() == 2);
        writeNamed(LogicalPlan.class, logicalPlan);
    }

    public void writePhysicalPlanNode(PhysicalPlan physicalPlan) throws IOException {
        assert physicalPlan.children().size() <= 1;
        writeNamed(PhysicalPlan.class, physicalPlan);
    }

    public void writeOptionalPhysicalPlanNode(PhysicalPlan physicalPlan) throws IOException {
        if (physicalPlan == null) {
            writeBoolean(false);
        } else {
            writeBoolean(true);
            writePhysicalPlanNode(physicalPlan);
        }
    }

    public  void writeNamed(Class type, T value) throws IOException {
        String name = nameSupplier.apply(value.getClass());
        @SuppressWarnings("unchecked")
        PlanWriter writer = (PlanWriter) registry.getWriter(type, name);
        writeString(name);
        writer.write(this, value);
    }

    @Override
    public void writeByte(byte b) throws IOException {
        delegate.writeByte(b);
    }

    @Override
    public void writeBytes(byte[] b, int offset, int length) throws IOException {
        delegate.writeBytes(b, offset, length);
    }

    @Override
    public void flush() throws IOException {
        delegate.flush();
    }

    @Override
    public void close() throws IOException {
        delegate.close();
    }

    @Override
    public TransportVersion getTransportVersion() {
        return delegate.getTransportVersion();
    }

    @Override
    public void setTransportVersion(TransportVersion version) {
        delegate.setTransportVersion(version);
        super.setTransportVersion(version);
    }

    /**
     * Write a {@link Block} as part of the plan.
     * 

* These {@link Block}s are not tracked by {@link BlockFactory} and closing them * does nothing so they should be small. We do make sure not to send duplicates, * reusing blocks sent as part of the {@link EsqlConfiguration#tables()} if * possible, otherwise sending a {@linkplain Block} inline. *

*/ public void writeCachedBlock(Block block) throws IOException { assert block instanceof LongBigArrayBlock == false : "BigArrays not supported because we don't close"; assert block instanceof IntBigArrayBlock == false : "BigArrays not supported because we don't close"; assert block instanceof DoubleBigArrayBlock == false : "BigArrays not supported because we don't close"; assert block instanceof BooleanBigArrayBlock == false : "BigArrays not supported because we don't close"; BytesReference key = cachedBlocks.get(block); if (key != null) { key.writeTo(this); return; } writeByte(NEW_BLOCK_KEY); writeVInt(nextCachedBlock); cachedBlocks.put(block, fromPreviousKey(nextCachedBlock)); writeNamedWriteable(block); nextCachedBlock++; } /** * The byte representing a {@link Block} sent for the first time. The byte * will be followed by a {@link StreamOutput#writeVInt} encoded identifier * and then the contents of the {@linkplain Block} will immediately follow * this byte. */ static final byte NEW_BLOCK_KEY = 0; /** * The byte representing a {@link Block} that has previously been sent. * This byte will be followed up a {@link StreamOutput#writeVInt} encoded * identifier pointing to the block to read. */ static final byte FROM_PREVIOUS_KEY = 1; /** * The byte representing a {@link Block} that was part of the * {@link EsqlConfiguration#tables()} map. It is followed a string for * the table name and then a string for the column name. */ static final byte FROM_CONFIG_KEY = 2; /** * Build the key for reading a {@link Block} from the cache of previously * received {@linkplain Block}s. */ static BytesReference fromPreviousKey(int id) throws IOException { try (BytesStreamOutput key = new BytesStreamOutput()) { key.writeByte(FROM_PREVIOUS_KEY); key.writeVInt(id); return key.bytes(); } } /** * Build the key for reading a {@link Block} from the {@link EsqlConfiguration}. * This is important because some operations like {@code LOOKUP} frequently read * {@linkplain Block}s directly from the configuration. *

* It'd be possible to implement this by adding all of the Blocks as "previous" * keys in the constructor and never use this construct at all, but that'd * require there be a consistent ordering of Blocks there. We could make one, * but I'm afraid that'd be brittle as we evolve the code. It'd make wire * compatibility difficult. This signal is much simpler to deal with even though * it is more bytes over the wire. *

*/ static BytesReference fromConfigKey(String table, String column) throws IOException { try (BytesStreamOutput key = new BytesStreamOutput()) { key.writeByte(FROM_CONFIG_KEY); key.writeString(table); key.writeString(column); return key.bytes(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy