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

tech.ytsaurus.client.RetryingTableWriterImpl Maven / Gradle / Ivy

The newest version!
package tech.ytsaurus.client;

import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import javax.annotation.Nullable;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tech.ytsaurus.client.request.CreateNode;
import tech.ytsaurus.client.request.GetNode;
import tech.ytsaurus.client.request.LockNode;
import tech.ytsaurus.client.request.StartTransaction;
import tech.ytsaurus.client.request.WriteTable;
import tech.ytsaurus.client.rows.EntityTableSchemaCreator;
import tech.ytsaurus.client.rows.UnversionedRow;
import tech.ytsaurus.client.rows.UnversionedRowSerializer;
import tech.ytsaurus.client.rpc.RpcOptions;
import tech.ytsaurus.client.rpc.RpcUtil;
import tech.ytsaurus.core.GUID;
import tech.ytsaurus.core.cypress.CypressNodeType;
import tech.ytsaurus.core.cypress.YPath;
import tech.ytsaurus.core.request.LockMode;
import tech.ytsaurus.core.tables.TableSchema;
import tech.ytsaurus.lang.NonNullApi;
import tech.ytsaurus.lang.NonNullFields;
import tech.ytsaurus.ysontree.YTreeNode;

@NonNullApi
@NonNullFields
class WriteTask {
    final byte[] data;
    final RetryPolicy retryPolicy;
    final CompletableFuture handled;
    final int index;

    WriteTask(Buffer buffer, TableRowsSerializer rowsSerializer, RetryPolicy retryPolicy, int index) {
        this.data = buffer.finish(rowsSerializer);
        this.retryPolicy = retryPolicy;
        this.handled = buffer.handled;
        this.index = index;
    }
}

@NonNullApi
@NonNullFields
class Buffer {
    final ByteBuf buffer;
    final CompletableFuture handled = new CompletableFuture<>();
    int rowsCount = 0;

    Buffer() {
        this.buffer = Unpooled.buffer();
    }

    public int size() {
        return buffer.readableBytes();
    }

    public void write(ByteBuf buf) {
        buffer.writeBytes(buf);
    }

    public byte[] finish(TableRowsSerializer rowsSerializer) {
        try {
            return rowsSerializer.serializeRowsWithDescriptor(buffer, rowsCount);
        } catch (IOException ex) {
            throw new RuntimeException("Serialization was failed, but it wasn't expected");
        }
    }
}

@NonNullApi
@NonNullFields
class InitResult {
    ApiServiceTransaction transaction;
    TableSchema schema;

    InitResult(ApiServiceTransaction transaction, TableSchema schema) {
        this.transaction = transaction;
        this.schema = schema;
    }
}

interface Abortable {
    T abort();
}

@NonNullApi
@NonNullFields
class RetryingTableWriterBaseImpl {
    static final Logger logger = LoggerFactory.getLogger(RetryingTableWriterImpl.class);

    final ApiServiceClient apiServiceClient;
    final ScheduledExecutorService executor;
    WriteTable secondaryReq;
    final RpcOptions rpcOptions;
    @Nullable
    TableRowsSerializer tableRowsSerializer;

    final Queue> writeTasks = new ConcurrentLinkedQueue<>();
    final Set> processing = new HashSet<>();
    final Queue> handledEvents = new ConcurrentLinkedQueue<>();

    final Semaphore semaphore;

    final CompletableFuture init;
    final CompletableFuture result = new CompletableFuture<>();
    final CompletableFuture firstBufferHandled;

    volatile WriteTable req;
    @Nullable
    private volatile Buffer buffer;

    volatile boolean canceled = false;
    volatile boolean closed = false;
    int nextWriteTaskIndex = 0;

    volatile CompletableFuture readyEvent = CompletableFuture.completedFuture(null);

    RetryingTableWriterBaseImpl(
            ApiServiceClient apiServiceClient,
            ScheduledExecutorService executor,
            WriteTable req,
            RpcOptions rpcOptions,
            SerializationResolver serializationResolver
    ) {
        req = needSetTableSchema(req) ? getRequestWithTableSchema(req) : req;

        this.apiServiceClient = apiServiceClient;
        this.executor = executor;
        this.rpcOptions = rpcOptions;

        this.req = req.toBuilder().setNeedRetries(false).build();
        this.secondaryReq = this.req.toBuilder().setPath(req.getYPath().append(true)).build();

        YPath path = this.req.getYPath();
        boolean append = path.getAppend().orElse(false);
        LockMode lockMode = append ? LockMode.Shared : LockMode.Exclusive;

        this.semaphore = new Semaphore(this.req.getMaxWritesInFlight());

        Buffer firstBuffer = new Buffer<>();
        firstBufferHandled = firstBuffer.handled;
        this.buffer = firstBuffer;

        StartTransaction.Builder transactionRequestBuilder = StartTransaction.master().toBuilder();
        req.getTransactionId().ifPresent(transactionRequestBuilder::setParentId);
        this.init = apiServiceClient.startTransaction(transactionRequestBuilder.build())
                .thenCompose(transaction -> {
                    CompletableFuture createNodeFuture;
                    if (!append) {
                        HashMap attributes = new HashMap<>();
                        this.req.getTableSchema()
                                .ifPresent(schema -> attributes.put("schema", schema.toYTree()));
                        createNodeFuture = transaction.createNode(
                                CreateNode.builder()
                                        .setPath(path)
                                        .setType(CypressNodeType.TABLE)
                                        .setAttributes(attributes)
                                        .setIgnoreExisting(true)
                                        .build());
                    } else {
                        createNodeFuture = CompletableFuture.completedFuture(transaction);
                    }
                    return createNodeFuture
                            .thenCompose(unused -> transaction.lockNode(new LockNode(path, lockMode)))
                            .thenCompose(unused -> transaction.getNode(
                                    GetNode.builder()
                                            .setPath(path.justPath())
                                            .setAttributes(List.of("schema"))
                                            .build()))
                            .thenApply(node -> new InitResult(
                                    transaction, TableSchema.fromYTree(node.getAttributeOrThrow("schema"))))
                            .thenApply(result -> {
                                this.req.getSerializationContext()
                                        .getSkiffSerializer()
                                        .ifPresent(serializer -> serializer.setTableSchema(result.schema));

                                if (this.req.getTableSchema().isEmpty()) {
                                    if (this.req.getSerializationContext().getSkiffSerializer().isPresent()) {
                                        this.req = this.req.toBuilder()
                                                .setTableSchema(result.schema)
                                                .build();
                                        this.secondaryReq = this.secondaryReq.toBuilder()
                                                .setTableSchema(result.schema)
                                                .build();
                                    }
                                }

                                this.tableRowsSerializer = TableRowsSerializer.createTableRowsSerializer(
                                        this.req.getSerializationContext(), serializationResolver).orElse(null);
                                if (this.tableRowsSerializer == null) {
                                    if (this.req.getSerializationContext().getObjectClass().isEmpty()) {
                                        throw new IllegalStateException("No object clazz");
                                    }
                                    Class objectClazz = this.req.getSerializationContext().getObjectClass().get();
                                    if (UnversionedRow.class.equals(objectClazz)) {
                                        this.tableRowsSerializer =
                                                (TableRowsSerializer) new TableRowsWireSerializer<>(
                                                        new UnversionedRowSerializer());
                                    } else {
                                        this.tableRowsSerializer = new TableRowsWireSerializer<>(
                                                serializationResolver.createWireRowSerializer(
                                                        serializationResolver.forClass(objectClazz, result.schema)));
                                    }
                                }
                                return result;
                            });
                });

        this.init.handle((initResult, ex) -> {
            if (ex != null) {
                cancel();
            }
            return null;
        });
    }

    public TableSchema getSchema() {
        if (tableRowsSerializer == null) {
            throw new RuntimeException("No tableRowsSerializer in TableWriter");
        }
        return tableRowsSerializer.getSchema();
    }

    private boolean addAbortable(Abortable abortable) {
        boolean wantAbort = false;
        synchronized (this) {
            if (canceled) {
                wantAbort = true;
            }
            processing.add(abortable);
        }
        if (wantAbort) {
            abortable.abort();
            return true;
        }
        return false;
    }

    private synchronized void removeAbortable(Abortable abortable) {
        processing.remove(abortable);
    }

    private , U> CompletableFuture tryWith(
            CompletableFuture abortableFuture,
            Function> function
    ) {
        return abortableFuture.thenCompose(abortable -> {
            if (addAbortable(abortable)) {
                // No need to call `function`.
                return RpcUtil.failedFuture(new IllegalStateException("Already canceled"));
            }

            CompletableFuture functionResult = function.apply(abortable);
            functionResult.whenComplete((res, ex) -> {
                if (ex != null) {
                    abortable.abort();
                }
                removeAbortable(abortable);
            });
            return functionResult;
        });
    }

    private boolean needSetTableSchema(WriteTable req) {
        return req.getSerializationContext().getSkiffSerializer().isPresent() &&
                !req.getYPath().getAppend().orElse(false) &&
                req.getTableSchema().isEmpty();
    }

    private WriteTable getRequestWithTableSchema(WriteTable req) {
        return req.toBuilder()
                .setTableSchema(
                        EntityTableSchemaCreator.create(
                                req.getSerializationContext()
                                        .getObjectClass()
                                        .orElseThrow(IllegalStateException::new),
                                null
                        )
                )
                .build();
    }

    private  U checkedGet(CompletableFuture future) {
        if (!future.isDone() && future.isCompletedExceptionally()) {
            throw new IllegalArgumentException("internal error");
        }
        return future.join();
    }

    private void processWriteTask(WriteTask writeTask) {
        if (canceled) {
            // `RetryingWriter.cancel()` was called, no need to process buffers more.
            return;
        }

        writeTask.retryPolicy.onNewAttempt();

        GUID parentTxId = checkedGet(init).transaction.getId();
        CompletableFuture localTransactionFuture = apiServiceClient.startTransaction(
                StartTransaction.master().toBuilder().setParentId(parentTxId).build());

        tryWith(localTransactionFuture, localTransaction -> {
            CompletableFuture writerFuture = localTransaction.writeTable(req)
                    .thenApply(writer -> (RawTableWriter) writer);

            return tryWith(writerFuture, writer -> writer.readyEvent()
                    .thenCompose(unused -> {
                        boolean writeResult = writer.write(writeTask.data);
                        if (!writeResult) {
                            throw new IllegalStateException("internal error");
                        }
                        return writer.finish();
                    }).thenCompose(unused -> localTransaction.commit()).thenApply(unused -> {
                        this.req = this.secondaryReq;
                        writeTask.handled.complete(null);
                        semaphore.release();

                        if (!writeTasks.isEmpty()) {
                            tryStartProcessWriteTask();
                        }
                        return null;
                    })
            );
        }).handle((unused, ex) -> {
            if (ex == null) {
                return null;
            }

            Optional backoff = writeTask.retryPolicy.getBackoffDuration(ex, rpcOptions);
            if (backoff.isEmpty()) {
                writeTask.handled.completeExceptionally(ex);
                result.completeExceptionally(ex);
                return null;
            }

            logger.debug("Got error, we will retry it in {} seconds, message='{}'",
                    backoff.get().toNanos() / 1000000000, ex.getMessage());

            executor.schedule(
                    () -> processWriteTask(writeTask),
                    backoff.get().toNanos(),
                    TimeUnit.NANOSECONDS);

            return null;
        });
    }

    private void tryStartProcessWriteTask() {
        if (!semaphore.tryAcquire()) {
            // Max flights inflight limit was reached.
            return;
        }

        executor.execute(() -> {
            WriteTask writeTask = writeTasks.peek();
            if (writeTask == null) {
                semaphore.release();
                return;
            }

            if (!firstBufferHandled.isDone() && writeTask.index > 0) {
                // This means that that first buffer is already being processed, but it isn't finished yet.
                semaphore.release();
                return;
            }

            writeTask = writeTasks.poll();
            if (writeTask == null) {
                semaphore.release();
                return;
            }

            synchronized (this) {
                if (canceled) {
                    return;
                }
                if (buffer == null && !closed) {
                    buffer = new Buffer<>();
                    readyEvent.complete(null);
                }
            }

            processWriteTask(writeTask);
        });
    }

    private void flushBuffer(boolean lastBuffer) {
        Buffer currentBuffer = buffer;
        if (currentBuffer == null) {
            return;
        }

        if (lastBuffer && currentBuffer.size() == 0) {
            buffer = null;
            return;
        }

        WriteTask writeTask = new WriteTask<>(
                currentBuffer, tableRowsSerializer, rpcOptions.getRetryPolicyFactory().get(), nextWriteTaskIndex++);
        writeTasks.add(writeTask);
        handledEvents.add(currentBuffer.handled);

        if (!lastBuffer && writeTasks.size() <= req.getMaxWritesInFlight()) {
            buffer = new Buffer<>();
        } else {
            buffer = null;
            readyEvent = new CompletableFuture<>();
        }

        tryStartProcessWriteTask();
    }

    public boolean write(List rows, TableSchema schema) {
        if (!init.isDone() || result.isCompletedExceptionally()) {
            return false;
        }

        if (closed || canceled) {
            return false;
        }

        Buffer currentBuffer = buffer;
        if (currentBuffer == null) {
            return false;
        }

        ByteBuf serializedRows = tableRowsSerializer.serializeRowsToBuf(rows, schema);
        currentBuffer.write(serializedRows);
        currentBuffer.rowsCount += rows.size();

        if (currentBuffer.size() >= req.getChunkSize()) {
            flushBuffer(false);
        }

        return true;
    }

    public CompletableFuture readyEvent() {
        if (result.isCompletedExceptionally()) {
            return result;
        }

        return CompletableFuture.anyOf(result, CompletableFuture.allOf(init, readyEvent))
                .thenCompose(unused -> CompletableFuture.completedFuture(null));
    }

    public CompletableFuture close() {
        synchronized (this) {
            closed = true;
            flushBuffer(true);
        }

        return init.thenCompose(initResult -> CompletableFuture.anyOf(result, CompletableFuture.allOf(
                        handledEvents.toArray(new CompletableFuture[0]))).thenApply(unused -> initResult)
                ).thenCompose(initResult -> initResult.transaction.commit())
                .whenComplete((unused, ex) -> {
                    // Exception will be saved after this stage.
                    if (ex != null) {
                        cancel();
                    }
                });
    }

    public CompletableFuture getTableSchema() {
        return init.thenApply(initResult -> initResult.schema);
    }

    public synchronized void cancel() {
        canceled = true;
        buffer = null;
        readyEvent = new CompletableFuture<>();

        for (Abortable abortable : processing) {
            abortable.abort();
        }

        init.join().transaction.abort();
    }
}

@NonNullApi
@NonNullFields
class RetryingTableWriterImpl extends RetryingTableWriterBaseImpl implements TableWriter {
    RetryingTableWriterImpl(
            ApiServiceClient apiServiceClient,
            ScheduledExecutorService executor,
            WriteTable req,
            RpcOptions rpcOptions,
            SerializationResolver serializationResolver
    ) {
        super(apiServiceClient, executor, req, rpcOptions, serializationResolver);
    }

    @Override
    public boolean write(List rows, TableSchema schema) {
        return super.write(rows, schema);
    }

    @Override
    public CompletableFuture readyEvent() {
        return super.readyEvent();
    }

    @Override
    public CompletableFuture close() {
        return super.close();
    }

    @Override
    public CompletableFuture getTableSchema() {
        return super.getTableSchema();
    }

    @Override
    public TableSchema getSchema() {
        return super.getSchema();
    }

    @Override
    public synchronized void cancel() {
        super.cancel();
    }
}

@NonNullApi
@NonNullFields
class AsyncRetryingTableWriterImpl extends RetryingTableWriterBaseImpl implements AsyncWriter {
    AsyncRetryingTableWriterImpl(
            ApiServiceClient apiServiceClient,
            ScheduledExecutorService executor,
            WriteTable req,
            RpcOptions rpcOptions,
            SerializationResolver serializationResolver
    ) {
        super(apiServiceClient, executor, req, rpcOptions, serializationResolver);
    }

    @Override
    public CompletableFuture write(List rows) {
        return init.thenCompose(initResult -> {
            Objects.requireNonNull(tableRowsSerializer);
            TableSchema schema;
            if (req.getTableSchema().isPresent()) {
                schema = req.getTableSchema().get();
            } else if (tableRowsSerializer.getSchema().getColumnsCount() > 0) {
                schema = tableRowsSerializer.getSchema();
            } else {
                schema = initResult.schema;
            }

            return writeImpl(rows, schema);
        });
    }

    private CompletableFuture writeImpl(List rows, TableSchema schema) {
        if (write(rows, schema)) {
            return CompletableFuture.completedFuture(null);
        }
        return readyEvent().thenCompose(unused -> writeImpl(rows, schema));
    }

    @Override
    public CompletableFuture finish() {
        return super.close();
    }
}