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

org.restheart.mongodb.db.sessions.TxnsUtils Maven / Gradle / Ivy

There is a newer version: 8.1.5
Show newest version
/*-
 * ========================LICENSE_START=================================
 * restheart-mongodb
 * %%
 * Copyright (C) 2014 - 2024 SoftInstigate
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 * =========================LICENSE_END==================================
 */
package org.restheart.mongodb.db.sessions;

import com.mongodb.ClientSessionOptions;
import com.mongodb.client.MongoClient;
import com.mongodb.MongoQueryException;

import static com.mongodb.client.model.Filters.eq;

import java.util.Optional;
import java.util.UUID;

import org.restheart.mongodb.RHMongoClients;
import org.restheart.mongodb.RSOps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Andrea Di Cesare {@literal }
 */
public class TxnsUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(TxnsUtils.class);

    private static final MongoClient MCLIENT = RHMongoClients.mclient();

    /**
     * Warn: requires two round trips to server
     *
     * @param sid
     * @return the txn status from server
     */
    public static Txn getTxnServerStatus(UUID sid, Optional rsOps) {
        var options = Sid.getSessionOptions(sid);

        var cso = ClientSessionOptions
            .builder()
            .causallyConsistent(options.isCausallyConsistent())
            .build();

        var cs = TxnClientSessionFactory.getInstance().createClientSession(sid, rsOps, cso);

        // set txnId on ServerSession
        if (cs.getServerSession().getTransactionNumber() < 1) {
            ((ServerSessionImpl) cs.getServerSession()).setTransactionNumber(1);
        }

        try {
            if (!cs.hasActiveTransaction()) {
                cs.setMessageSentInCurrentTransaction(true);
                cs.startTransaction();
            }
            propagateSession(cs);
            return new Txn(1, Txn.TransactionStatus.IN);
        } catch (MongoQueryException mqe) {
            var num = getTxnNumFromExc(mqe);

            if (num > 1) {
                cs.setServerSessionTransactionNumber(num);

                try {
                    propagateSession(cs);
                    return new Txn(num, Txn.TransactionStatus.IN);
                } catch (MongoQueryException mqe2) {
                    return new Txn(num, getTxnStateFromExc(mqe2));
                }
            } else {
                return new Txn(num, getTxnStateFromExc(mqe));
            }
        }
    }

    /**
     * Warn: requires a round trip to the server
     *
     * @param cs
     * @throws MongoQueryException
     */
    public static void propagateSession(ClientSessionImpl cs) throws MongoQueryException {
        LOGGER.trace("*********** round trip to server to propagate session");
        MCLIENT.getDatabase("foo").getCollection("bar")
            .find(cs)
            .limit(1)
            .projection(eq("_id", 1))
            .first();
    }

    private static final String TXT_NUM_ERROR_MSG_PREFIX_STARTED = "because a newer transaction ";
    private static final String TXT_NUM_ERROR_MSG_SUFFIX_STARTED = " has already started";

    private static final String TXT_NUM_ERROR_MSG_PREFIX_ABORTED = "Transaction ";
    private static final String TXT_NUM_ERROR_MSG_SUFFIX_ABORTED = " has been aborted";

    private static final String TXT_NUM_ERROR_MSG_PREFIX_COMMITTED = "Transaction ";
    private static final String TXT_NUM_ERROR_MSG_SUFFIX_COMMITTED = " has been committed";

    private static final String TXT_NUM_ERROR_MSG_PREFIX_NONE = "Given transaction number ";
    private static final String TXT_NUM_ERROR_MSG_SUFFIX_NONE = " does not match any in-progress transactions";

    /**
     * gets the actual txn id from error messages
     *
     * @param mqe
     * @return
     */
    private static long getTxnNumFromExc(MongoQueryException mqe) {
        if (mqe.getErrorCode() == 225 && mqe.getErrorMessage() != null) {
            int start = mqe.getErrorMessage().indexOf(TXT_NUM_ERROR_MSG_PREFIX_STARTED) + TXT_NUM_ERROR_MSG_PREFIX_STARTED.length();
            int end = mqe.getErrorMessage().indexOf(TXT_NUM_ERROR_MSG_SUFFIX_STARTED);

            if (start >= 0 && end >= 0) {
                String numStr = mqe.getErrorMessage().substring(start, end).strip();
                numStr = removeWithTxnNumber(numStr);

                return Long.parseLong(numStr);
            }
        } else if (mqe.getErrorCode() == 251 && mqe.getErrorMessage() != null) {
            int start = mqe.getErrorMessage().indexOf(TXT_NUM_ERROR_MSG_PREFIX_ABORTED) + TXT_NUM_ERROR_MSG_PREFIX_ABORTED.length();
            int end = mqe.getErrorMessage().indexOf(TXT_NUM_ERROR_MSG_SUFFIX_ABORTED);

            if (start >= 0 && end >= 0) {
                String numStr = mqe.getErrorMessage().substring(start, end).strip();
                numStr = removeWithTxnNumber(numStr);

                return Long.parseLong(numStr);
            }

            start = mqe.getErrorMessage().indexOf(TXT_NUM_ERROR_MSG_PREFIX_NONE) + TXT_NUM_ERROR_MSG_PREFIX_NONE.length();
            end = mqe.getErrorMessage().indexOf(TXT_NUM_ERROR_MSG_SUFFIX_NONE);

            if (start >= 0 && end >= 0) {
                String numStr = mqe.getErrorMessage().substring(start, end).strip();
                numStr = removeWithTxnNumber(numStr);

                return Long.parseLong(numStr);
            }
        } else if (mqe.getErrorCode() == 256 && mqe.getErrorMessage() != null) {
            int start = mqe.getErrorMessage().indexOf(TXT_NUM_ERROR_MSG_PREFIX_COMMITTED) + TXT_NUM_ERROR_MSG_PREFIX_COMMITTED.length();
            int end = mqe.getErrorMessage().indexOf(TXT_NUM_ERROR_MSG_SUFFIX_COMMITTED);

            if (start >= 0 && end >= 0) {
                String numStr = mqe.getErrorMessage().substring(start, end).strip();
                numStr = removeWithTxnNumber(numStr);

                return Long.parseLong(numStr);
            }
        }

        LOGGER.trace("***** query error not handled {}: {}",
                mqe.getErrorCode(),
                mqe.getErrorMessage());

        throw mqe;
    }

    private static final String TXN = "txnNumber";
    /**
     * errorMsg can be the transaction number or:
     * - from MongoDb 6:   with txnNumberAndRetryCounter { txnNumber: 10, txnRetryCounter: 0 }
     * - from MongoDB 5:   with { txnNumber: 10 }
     * - from MongoDB < 5: with txnNumber 10
     * @param errorMsg
     * @return
     */
    static String removeWithTxnNumber(String errorMsg) {
        if (errorMsg == null) {
            throw new IllegalArgumentException("argument s cannot be null");
        }

        if (errorMsg.contains(TXN)) {
            var t = errorMsg.substring(errorMsg.lastIndexOf(TXN) + TXN.length()).strip();

            if (t.indexOf(":") >= 0) {
                t = t.substring(t.indexOf(":")+1);
            }

            var closingBracket = t.indexOf("}");
            var comma = t.indexOf(",");

            if (closingBracket > 0 || comma > 0) {
                return t.substring(0, comma > 0 ? comma : closingBracket).strip();
            } else {
                return t;
            }
        } else {
            return errorMsg;
        }
    }

    /**
     * get txn status from error messag
     *
     * @param mqe
     * @return
     */
    private static Txn.TransactionStatus getTxnStateFromExc(MongoQueryException mqe) {
        if (mqe.getErrorCode() == 251) {
            if (mqe.getErrorMessage().contains("does not match any in-progress transactions")) {
                return Txn.TransactionStatus.NONE;
            } else if (mqe.getErrorMessage().contains("has been aborted")) {
                return Txn.TransactionStatus.ABORTED;
            }
        } else if (mqe.getErrorCode() == 256) {
            if (mqe.getErrorMessage().contains("has been committed")) {
                return Txn.TransactionStatus.COMMITTED;
            }
        }

        LOGGER.trace("***** query error not handled {}: {}", mqe.getErrorCode(), mqe.getErrorMessage());

        throw mqe;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy