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

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

The newest version!
package tech.ytsaurus.client;

import java.time.Duration;
import java.util.AbstractQueue;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tech.ytsaurus.client.operations.Operation;
import tech.ytsaurus.client.operations.Spec;
import tech.ytsaurus.client.operations.SpecPreparationContext;
import tech.ytsaurus.client.request.AbstractLookupRowsRequest;
import tech.ytsaurus.client.request.AbstractModifyRowsRequest;
import tech.ytsaurus.client.request.AdvanceConsumer;
import tech.ytsaurus.client.request.CheckPermission;
import tech.ytsaurus.client.request.ConcatenateNodes;
import tech.ytsaurus.client.request.CopyNode;
import tech.ytsaurus.client.request.CreateNode;
import tech.ytsaurus.client.request.ExistsNode;
import tech.ytsaurus.client.request.GetFileFromCache;
import tech.ytsaurus.client.request.GetFileFromCacheResult;
import tech.ytsaurus.client.request.GetNode;
import tech.ytsaurus.client.request.LinkNode;
import tech.ytsaurus.client.request.ListNode;
import tech.ytsaurus.client.request.LockNode;
import tech.ytsaurus.client.request.LockNodeResult;
import tech.ytsaurus.client.request.MapOperation;
import tech.ytsaurus.client.request.MapReduceOperation;
import tech.ytsaurus.client.request.MergeOperation;
import tech.ytsaurus.client.request.MoveNode;
import tech.ytsaurus.client.request.MultiLookupRowsRequest;
import tech.ytsaurus.client.request.MultiTablePartition;
import tech.ytsaurus.client.request.PartitionTables;
import tech.ytsaurus.client.request.PutFileToCache;
import tech.ytsaurus.client.request.PutFileToCacheResult;
import tech.ytsaurus.client.request.ReadFile;
import tech.ytsaurus.client.request.ReadTable;
import tech.ytsaurus.client.request.ReduceOperation;
import tech.ytsaurus.client.request.RemoteCopyOperation;
import tech.ytsaurus.client.request.RemoveNode;
import tech.ytsaurus.client.request.SelectRowsRequest;
import tech.ytsaurus.client.request.SetNode;
import tech.ytsaurus.client.request.SortOperation;
import tech.ytsaurus.client.request.StartOperation;
import tech.ytsaurus.client.request.TransactionalOptions;
import tech.ytsaurus.client.request.VanillaOperation;
import tech.ytsaurus.client.request.WriteFile;
import tech.ytsaurus.client.request.WriteTable;
import tech.ytsaurus.client.rows.ConsumerSource;
import tech.ytsaurus.client.rows.UnversionedRowset;
import tech.ytsaurus.client.rows.VersionedRowset;
import tech.ytsaurus.core.GUID;
import tech.ytsaurus.core.YtTimestamp;
import tech.ytsaurus.core.common.YTsaurusError;
import tech.ytsaurus.core.common.YTsaurusErrorCode;
import tech.ytsaurus.core.rows.YTreeRowSerializer;
import tech.ytsaurus.rpcproxy.EOperationType;
import tech.ytsaurus.rpcproxy.TCheckPermissionResult;
import tech.ytsaurus.ysontree.YTree;
import tech.ytsaurus.ysontree.YTreeBuilder;
import tech.ytsaurus.ysontree.YTreeNode;

public class ApiServiceTransaction implements TransactionalClient, AutoCloseable, Abortable {
    enum State {
        ACTIVE,
        COMMITTING,
        COMMITTED,
        CLOSED,
    }

    private static final Logger logger = LoggerFactory.getLogger(ApiServiceTransaction.class);

    private final ApiServiceClientImpl client;
    private final GUID id;
    private final YtTimestamp startTimestamp;
    private final boolean ping;
    private final boolean sticky;
    private final TransactionalOptions transactionalOptions;
    private final Duration pingPeriod;
    private final Duration failedPingRetryPeriod;
    private final ScheduledExecutorService executor;
    private final CompletableFuture transactionCompleteFuture = new CompletableFuture<>();
    private final AtomicReference state = new AtomicReference<>(State.ACTIVE);
    private final AtomicReference forcedPingStop = new AtomicReference<>(false);
    private final AbstractQueue> modifyRowsResults = new ConcurrentLinkedQueue<>();
    private final Consumer onPingFailed;

    @SuppressWarnings("checkstyle:ParameterNumber")
    ApiServiceTransaction(
            ApiServiceClientImpl client,
            GUID id,
            YtTimestamp startTimestamp,
            boolean ping,
            boolean pingAncestors,
            boolean sticky,
            Duration pingPeriod,
            ScheduledExecutorService executor
    ) {
        this(
                client,
                id,
                startTimestamp,
                ping,
                pingAncestors,
                sticky,
                pingPeriod,
                null,
                executor,
                null
        );
    }

    @SuppressWarnings("checkstyle:ParameterNumber")
    ApiServiceTransaction(
            ApiServiceClientImpl client,
            GUID id,
            YtTimestamp startTimestamp,
            boolean ping,
            boolean pingAncestors,
            boolean sticky,
            Duration pingPeriod,
            Duration failedPingRetryPeriod,
            ScheduledExecutorService executor,
            Consumer onPingFailed
    ) {
        this.client = client;
        this.id = Objects.requireNonNull(id);
        this.startTimestamp = Objects.requireNonNull(startTimestamp);
        this.ping = ping;
        this.sticky = sticky;
        this.transactionalOptions = new TransactionalOptions(id, sticky);
        this.pingPeriod = pingPeriod;
        this.failedPingRetryPeriod = isValidPingPeriod(failedPingRetryPeriod) ? failedPingRetryPeriod : pingPeriod;
        this.executor = executor;
        this.onPingFailed = onPingFailed;

        if (isValidPingPeriod(pingPeriod)) {
            executor.schedule(this::runPeriodicPings, pingPeriod.toMillis(), TimeUnit.MILLISECONDS);
        }
    }

    @Override
    public TransactionalClient getRootClient() {
        return client;
    }

    @Override
    public String toString() {
        return "Transaction(" + client + ")@" + id;
    }

    public ApiServiceClient getClient() {
        return client;
    }

    public GUID getId() {
        return id;
    }

    public TransactionalOptions getTransactionalOptions() {
        return transactionalOptions;
    }

    public YtTimestamp getStartTimestamp() {
        return startTimestamp;
    }

    public boolean isPing() {
        return ping;
    }

    public boolean isSticky() {
        return sticky;
    }

    CompletableFuture getTransactionCompleteFuture() {
        return transactionCompleteFuture;
    }

    boolean isActive() {
        return state.get() == State.ACTIVE;
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    private boolean isPingableState() {
        State currentState = state.get();
        return (currentState == State.ACTIVE || currentState == State.COMMITTING) && !forcedPingStop.get();
    }

    public void stopPing() {
        forcedPingStop.set(true);
    }

    private void runPeriodicPings() {
        if (!isPingableState()) {
            return;
        }

        ping().whenComplete((unused, ex) -> {
            long nextPingDelayMs;

            if (ex == null) {
                nextPingDelayMs = pingPeriod.toMillis();
            } else {
                nextPingDelayMs = failedPingRetryPeriod.toMillis();

                if (onPingFailed != null) {
                    onPingFailed.accept(ex instanceof Exception ? (Exception) ex : new RuntimeException(ex));
                }
                if (ex instanceof YTsaurusError) {
                    if (((YTsaurusError) ex).matches(YTsaurusErrorCode.NoSuchTransaction.getCode())) {
                        return;
                    }
                }
            }

            if (!isPingableState()) {
                return;
            }
            executor.schedule(this::runPeriodicPings, nextPingDelayMs, TimeUnit.MILLISECONDS);
        });
    }

    public CompletableFuture ping() {
        return client.pingTransaction(id);
    }

    private void throwWrongState(State expectedOldState, State newState) {
        // Yep, this state is outdated but Java8 doesn't have compareAndExchange,
        // so we do our best here. In any case it's a bug.
        State currentState = state.get();
        throw new IllegalStateException(
                String.format(
                        "Failed to set transaction into '%s' state; " +
                                "expected state: '%s'; current state (maybe outdated): '%s'",
                        newState,
                        expectedOldState,
                        currentState
                ));
    }

    private void updateState(State expectedOldState, State newState) {
        boolean set = state.compareAndSet(expectedOldState, newState);
        if (!set) {
            throwWrongState(expectedOldState, newState);
        }
    }

    public CompletableFuture commit() {
        updateState(State.ACTIVE, State.COMMITTING);

        List> allModifyRowsResults = new ArrayList<>();
        CompletableFuture current;
        while ((current = modifyRowsResults.poll()) != null) {
            allModifyRowsResults.add(current);
        }

        CompletableFuture allModifiesCompleted = CompletableFuture.allOf(
                allModifyRowsResults.toArray(new CompletableFuture[0])
        );

        return allModifiesCompleted.whenComplete((unused, error) -> {
            if (error != null) {
                logger.warn("Cannot commit transaction since modify rows failed:", error);
                abortImpl(false);
            }
        }).thenCompose(unused -> client.commitTransaction(id)
                .whenComplete((result, error) -> {
                    if (error == null) {
                        updateState(State.COMMITTING, State.COMMITTED);
                    } else {
                        state.set(State.CLOSED);
                    }
                    transactionCompleteFuture.complete(null);
                }));
    }

    public CompletableFuture abort() {
        return abortImpl(true);
    }

    private CompletableFuture abortImpl(boolean complainWrongState) {
        State oldState = state.getAndSet(State.CLOSED);
        if (oldState == State.ACTIVE || oldState == State.COMMITTING && !complainWrongState) {
            // Don't wait for answer.
            return client.abortTransaction(id)
                    .whenComplete((result, error) -> transactionCompleteFuture.complete(null));
        } else if (complainWrongState) {
            throwWrongState(State.ACTIVE, State.CLOSED);
        }

        return CompletableFuture.completedFuture(null);
    }

    /**
     * Aborts transaction unless it was committed or aborted before.
     */
    @Override
    public void close() {
        // NB. We intentionally ignore all errors of abort.
        abortImpl(false);
    }

    @Override
    public CompletableFuture lookupRows(AbstractLookupRowsRequest request) {
        return client.lookupRows(request.toBuilder().setTimestamp(startTimestamp).build());
    }

    @Override
    public  CompletableFuture> lookupRows(
            AbstractLookupRowsRequest request,
            YTreeRowSerializer serializer
    ) {
        return client.lookupRows(request.toBuilder().setTimestamp(startTimestamp).build(), serializer);
    }

    @Override
    public CompletableFuture> multiLookupRows(MultiLookupRowsRequest request) {
        return client.multiLookupRows(request.toBuilder().setTimestamp(startTimestamp).build());
    }

    @Override
    public  CompletableFuture>> multiLookupRows(
            MultiLookupRowsRequest request,
            YTreeRowSerializer serializer
    ) {
        return client.multiLookupRows(request.toBuilder().setTimestamp(startTimestamp).build(), serializer);
    }

    @Override
    public CompletableFuture versionedLookupRows(AbstractLookupRowsRequest request) {
        return client.versionedLookupRows(request.toBuilder().setTimestamp(startTimestamp).build());
    }

    @Override
    public CompletableFuture selectRows(String query) {
        return selectRows(SelectRowsRequest.of(query));
    }

    @Override
    public CompletableFuture selectRowsV2(SelectRowsRequest request) {
        return client.selectRowsV2(request.toBuilder().setTimestamp(startTimestamp).build());
    }

    @Override
    public CompletableFuture selectRows(SelectRowsRequest request) {
        return client.selectRows(request.toBuilder().setTimestamp(startTimestamp).build());
    }

    @Override
    public  CompletableFuture> selectRows(SelectRowsRequest request, YTreeRowSerializer serializer) {
        return client.selectRows(request.toBuilder().setTimestamp(startTimestamp).build(), serializer);
    }

    @Override
    public  CompletableFuture selectRows(SelectRowsRequest request, YTreeRowSerializer serializer,
                                                  ConsumerSource consumer) {
        return client.selectRows(request.toBuilder().setTimestamp(startTimestamp).build(), serializer, consumer);
    }

    public CompletableFuture modifyRows(AbstractModifyRowsRequest request) {
        // TODO: hide id to request
        CompletableFuture result = client.modifyRows(id, request);
        modifyRowsResults.add(result);
        return result;
    }

    public CompletableFuture modifyRows(AbstractModifyRowsRequest.Builder request) {
        // TODO: hide id to request
        CompletableFuture result = client.modifyRows(id, request);
        modifyRowsResults.add(result);
        return result;
    }

    public CompletableFuture advanceConsumer(AdvanceConsumer req) {
        return client.advanceConsumer(req.toBuilder().setTransactionId(id).build());
    }

    /* nodes */

    @Override
    public CompletableFuture createNode(CreateNode req) {
        return client.createNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture existsNode(ExistsNode req) {
        return client.existsNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture getNode(GetNode req) {
        return client.getNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture listNode(ListNode req) {
        return client.listNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture removeNode(RemoveNode req) {
        return client.removeNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture setNode(SetNode req) {
        return client.setNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture lockNode(LockNode req) {
        return client.lockNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture copyNode(CopyNode req) {
        return client.copyNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture moveNode(MoveNode req) {
        return client.moveNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture linkNode(LinkNode req) {
        return client.linkNode(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture concatenateNodes(ConcatenateNodes req) {
        return client.concatenateNodes(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture> partitionTables(PartitionTables req) {
        return client.partitionTables(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public  CompletableFuture> readTable(ReadTable req) {
        return client.readTable(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public  CompletableFuture> readTableV2(ReadTable req) {
        return client.readTableV2(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public  CompletableFuture> writeTable(WriteTable req) {
        return client.writeTable(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public  CompletableFuture> writeTableV2(WriteTable req) {
        return client.writeTableV2(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture readFile(ReadFile req) {
        return client.readFile(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture writeFile(WriteFile req) {
        return client.writeFile(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture startOperation(StartOperation req) {
        return client.startOperation(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    private CompletableFuture prepareSpec(Spec spec) {
        return CompletableFuture.supplyAsync(
                () -> {
                    YTreeBuilder builder = YTree.builder();
                    spec.prepare(builder, this, new SpecPreparationContext(client.getConfig()));
                    return client.patchSpec(builder.build().mapNode());
                },
                client.getPrepareSpecExecutor());
    }

    @Override
    public CompletableFuture startMap(MapOperation req) {
        var transactionalReq = req.toBuilder().setTransactionalOptions(transactionalOptions).build();
        return prepareSpec(transactionalReq.getSpec()).thenCompose(
                preparedSpec -> client.startPreparedOperation(preparedSpec, transactionalReq, EOperationType.OT_MAP)
        );
    }

    @Override
    public CompletableFuture startReduce(ReduceOperation req) {
        var transactionalReq = req.toBuilder().setTransactionalOptions(transactionalOptions).build();
        return prepareSpec(transactionalReq.getSpec()).thenCompose(
                preparedSpec -> client.startPreparedOperation(preparedSpec, transactionalReq, EOperationType.OT_REDUCE)
        );
    }

    @Override
    public CompletableFuture startSort(SortOperation req) {
        var transactionalReq = req.toBuilder().setTransactionalOptions(transactionalOptions).build();
        return prepareSpec(transactionalReq.getSpec()).thenCompose(
                preparedSpec -> client.startPreparedOperation(preparedSpec, transactionalReq, EOperationType.OT_SORT)
        );
    }

    @Override
    public CompletableFuture startMapReduce(MapReduceOperation req) {
        var transactionalReq = req.toBuilder().setTransactionalOptions(transactionalOptions).build();
        return prepareSpec(transactionalReq.getSpec()).thenCompose(
                preparedSpec -> client.startPreparedOperation(
                        preparedSpec, transactionalReq, EOperationType.OT_MAP_REDUCE)
        );
    }

    @Override
    public CompletableFuture startMerge(MergeOperation req) {
        var transactionalReq = req.toBuilder().setTransactionalOptions(transactionalOptions).build();
        return prepareSpec(transactionalReq.getSpec()).thenCompose(
                preparedSpec -> client.startPreparedOperation(preparedSpec, transactionalReq, EOperationType.OT_MERGE)
        );
    }

    @Override
    public CompletableFuture startRemoteCopy(RemoteCopyOperation req) {
        var transactionalReq = req.toBuilder().setTransactionalOptions(transactionalOptions).build();
        return prepareSpec(transactionalReq.getSpec()).thenCompose(
                preparedSpec -> client.startPreparedOperation(
                        preparedSpec, transactionalReq, EOperationType.OT_REMOTE_COPY)
        );
    }

    @Override
    public CompletableFuture startVanilla(VanillaOperation req) {
        var transactionalReq = req.toBuilder().setTransactionalOptions(transactionalOptions).build();
        return prepareSpec(transactionalReq.getSpec()).thenCompose(
                preparedSpec -> client.startPreparedOperation(preparedSpec, transactionalReq, EOperationType.OT_VANILLA)
        );
    }

    @Override
    public Operation attachOperation(GUID operationId) {
        return client.attachOperation(operationId);
    }

    @Override
    public CompletableFuture checkPermission(CheckPermission req) {
        return client.checkPermission(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture getFileFromCache(GetFileFromCache req) {
        return client.getFileFromCache(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    @Override
    public CompletableFuture putFileToCache(PutFileToCache req) {
        return client.putFileToCache(req.toBuilder().setTransactionalOptions(transactionalOptions).build());
    }

    /**
     * Return address of a proxy that is used for this transaction.
     */
    @Nullable
    String getRpcProxyAddress() {
        return client.getRpcProxyAddress();
    }

    private static boolean isValidPingPeriod(Duration pingPeriod) {
        return pingPeriod != null && !pingPeriod.isZero() && !pingPeriod.isNegative();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy