tech.ydb.yoj.repository.db.StdTxManager Maven / Gradle / Ivy
Show all versions of yoj-repository Show documentation
package tech.ydb.yoj.repository.db;
import com.google.common.base.Preconditions;
import io.prometheus.client.Counter;
import io.prometheus.client.Histogram;
import io.prometheus.client.Histogram.Timer;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.With;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import tech.ydb.yoj.repository.db.cache.TransactionLog;
import tech.ydb.yoj.repository.db.exception.RetryableException;
import tech.ydb.yoj.util.lang.CallStack;
import tech.ydb.yoj.util.lang.Strings;
import javax.annotation.Nullable;
import java.time.Duration;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static tech.ydb.yoj.repository.db.IsolationLevel.ONLINE_CONSISTENT_READ_ONLY;
import static tech.ydb.yoj.repository.db.IsolationLevel.SERIALIZABLE_READ_WRITE;
/**
* Standard implementation of {@link TxManager transaction manager} interface, which logs transaction statements and
* results, and reports transaction execution metrics: rollback and commit counts; attempt duration, total duration
* and retry count histograms.
* If you need a transaction manager, this is the right choice: just construct an instance of {@link StdTxManager}
* and use it.
*
If you need to decorate transaction execution logic with e.g. additional logging, tracing, rate limiting etc.,
* extend {@link DelegatingTxManager}, and override one or both of {@link DelegatingTxManager#doRunTx(Supplier)}
* and {@link DelegatingTxManager#wrapTxBody(Supplier) wrapTxBody()} methods.
*
* @see TxManager
* @see DelegatingTxManager
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class StdTxManager implements TxManager, TxManagerState {
public static volatile boolean useNewTxNameGeneration = false;
private static final Logger log = LoggerFactory.getLogger(StdTxManager.class);
private static final CallStack callStack = new CallStack();
private static final int DEFAULT_MAX_ATTEMPT_COUNT = 100;
private static final double[] TX_ATTEMPTS_BUCKETS = new double[]
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 25, 35, 40, 45, 50, 60, 70, 80, 90, 100};
private static final double[] DURATION_BUCKETS = {
.001, .0025, .005, .0075,
.01, .025, .05, .075,
.1, .25, .5, .75,
1, 2.5, 5, 7.5,
10, 25, 50, 75,
100
};
private static final Histogram totalDuration = Histogram.build("tx_total_duration_seconds", "Tx total duration (seconds)")
.labelNames("tx_name")
.buckets(DURATION_BUCKETS)
.register();
private static final Histogram attemptDuration = Histogram.build("tx_attempt_duration_seconds", "Tx attempt duration (seconds)")
.labelNames("tx_name")
.buckets(DURATION_BUCKETS)
.register();
private static final Histogram attempts = Histogram.build("tx_attempts", "Tx attempts spent to success")
.labelNames("tx_name")
.buckets(TX_ATTEMPTS_BUCKETS)
.register();
private static final Counter results = Counter.build("tx_result", "Tx commits/rollbacks/fails")
.labelNames("tx_name", "result")
.register();
private static final Counter retries = Counter.build("tx_retries", "Tx retry reasons")
.labelNames("tx_name", "reason")
.register();
private static final AtomicLong txLogIdSeq = new AtomicLong();
private static final Pattern PACKAGE_PATTERN = Pattern.compile(".*\\.");
private static final Pattern INNER_CLASS_PATTERN = Pattern.compile("\\$.*");
private static final Pattern SHORTEN_NAME_PATTERN = Pattern.compile("([A-Z][a-z]{2})[a-z]+");
@Getter
private final Repository repository;
@With(AccessLevel.PRIVATE)
private final int maxAttemptCount;
@With
private final String name;
@With(AccessLevel.PRIVATE)
private final Integer logLine;
@With
@Getter
private final String logContext;
@With(AccessLevel.PRIVATE)
private final TxOptions options;
@With(AccessLevel.PRIVATE)
private final SeparatePolicy separatePolicy;
@With
private final Set skipCallerPackages;
private final long txLogId = txLogIdSeq.incrementAndGet();
public StdTxManager(Repository repository) {
this(repository, DEFAULT_MAX_ATTEMPT_COUNT, null, null, null, TxOptions.create(SERIALIZABLE_READ_WRITE), SeparatePolicy.LOG, Set.of());
}
/**
* @deprecated Low-level constructor, kept for compatibility with existing code. Not recommended.
*/
@Deprecated
public StdTxManager(Repository repository, int maxAttemptCount, String name, Integer logLine, String logContext, TxOptions options) {
this(repository, maxAttemptCount, name, logLine, logContext, options, SeparatePolicy.LOG, Set.of());
}
@Override
public TxManager separate() {
return withSeparatePolicy(SeparatePolicy.ALLOW);
}
@Override
public TxManager delayedWrites() {
return withOptions(this.options.withImmediateWrites(false));
}
@Override
public TxManager immediateWrites() {
return withOptions(this.options.withImmediateWrites(true));
}
@Override
public TxManager noFirstLevelCache() {
return withOptions(this.options.withFirstLevelCache(false));
}
@Override
public TxManager failOnUnknownSeparateTx() {
return withSeparatePolicy(SeparatePolicy.STRICT);
}
@Override
public TxManager withMaxRetries(int maxRetries) {
Preconditions.checkArgument(maxRetries >= 0, "retry count must be >= 0");
return withMaxAttemptCount(1 + maxRetries);
}
@Override
public TxManager withDryRun(boolean dryRun) {
return withOptions(this.options.withDryRun(dryRun));
}
@Override
public TxManager withTimeout(Duration timeout) {
return withOptions(this.options.withTimeoutOptions(new TxOptions.TimeoutOptions(timeout)));
}
@Override
public TxManager withLogLevel(TransactionLog.Level level) {
return withOptions(this.options.withLogLevel(level));
}
@Override
public TxManager withLogStatementOnSuccess(boolean logStatementOnSuccess) {
return withOptions(this.options.withLogStatementOnSuccess(logStatementOnSuccess));
}
@Override
public ReadonlyBuilder readOnly() {
return new ReadonlyBuilderImpl(this.options.withIsolationLevel(ONLINE_CONSISTENT_READ_ONLY));
}
@Override
public ScanBuilder scan() {
return new ScanBuilderImpl(TxOptions.ScanOptions.DEFAULT);
}
@Override
public void tx(Runnable runnable) {
tx(() -> {
runnable.run();
return null;
});
}
@Override
public T tx(Supplier supplier) {
if (name == null) {
return withGeneratedNameAndLine().tx(supplier);
}
checkSeparatePolicy(separatePolicy, name);
return txImpl(supplier);
}
private T txImpl(Supplier supplier) {
RetryableException lastRetryableException = null;
TxImpl lastTx = null;
try (Timer ignored = totalDuration.labels(name).startTimer()) {
for (int attempt = 1; attempt <= maxAttemptCount; attempt++) {
try {
attempts.labels(name).observe(attempt);
T result;
try (var ignored1 = attemptDuration.labels(name).startTimer()) {
lastTx = new TxImpl(name, repository.startTransaction(options), options);
result = runAttempt(supplier, lastTx);
}
if (options.isDryRun()) {
results.labels(name, "rollback").inc();
results.labels(name, "dry_run").inc();
} else {
results.labels(name, "commit").inc();
}
return result;
} catch (RetryableException e) {
retries.labels(name, getExceptionNameForMetric(e)).inc();
lastRetryableException = e;
if (attempt + 1 <= maxAttemptCount) {
e.sleep(attempt);
}
} catch (Exception e) {
results.labels(name, "rollback").inc();
throw e;
}
}
results.labels(name, "fail").inc();
throw requireNonNull(lastRetryableException).rethrow();
} finally {
if (!options.isDryRun() && lastTx != null) {
lastTx.runDeferredFinally();
}
}
}
private static void checkSeparatePolicy(SeparatePolicy separatePolicy, String txName) {
if (!Tx.Current.exists()) {
return;
}
switch (separatePolicy) {
case ALLOW -> {
}
case STRICT ->
throw new IllegalStateException(format("Transaction %s was run when another transaction is active", txName));
case LOG ->
log.warn("Transaction '{}' was run when another transaction is active. Perhaps unexpected behavior. " +
"Use TxManager.separate() to avoid this message", txName);
}
}
private String getExceptionNameForMetric(RetryableException e) {
return Strings.removeSuffix(e.getClass().getSimpleName(), "Exception");
}
private T runAttempt(Supplier supplier, TxImpl tx) {
try (var ignored2 = MDC.putCloseable("tx", formatTx());
var ignored3 = MDC.putCloseable("tx-id", formatTxId());
var ignored4 = MDC.putCloseable("tx-name", formatTxName(false))) {
return tx.run(supplier);
}
}
private StdTxManager withGeneratedNameAndLine() {
return useNewTxNameGeneration ? withGeneratedNameAndLineNew() : withGeneratedNameAndLineOld();
}
private StdTxManager withGeneratedNameAndLineOld() {
String pkg = StdTxManager.class.getPackage().getName();
for (StackTraceElement ste : new Exception().getStackTrace()) {
if (!ste.getClassName().startsWith(pkg)) {
String name = ste.getClassName()
.replaceFirst(".*\\.", "")
.replaceFirst("\\$.*", "")
.replaceAll("([A-Z][a-z]{2})[a-z]+", "$1")
+ "#" + ste.getMethodName()
.replaceAll("([A-Z][a-z]{2})[a-z]+", "$1");
return withName(name).withLogLine(ste.getLineNumber());
}
}
throw new IllegalStateException("Stacktrace doesn't contain elements from other packages");
}
private StdTxManager withGeneratedNameAndLineNew() {
record TxInfo(String name, int lineNumber) {
}
var info = callStack.findCallingFrame()
.skipPackage(StdTxManager.class.getPackageName())
.skipPackages(skipCallerPackages)
.map(f -> new TxInfo(txName(f.getClassName(), f.getMethodName()), f.getLineNumber()));
return withName(info.name).withLogLine(info.lineNumber);
}
@NonNull
private static String txName(String className, String methodName) {
var cn = replaceFirst(className, PACKAGE_PATTERN, "");
cn = replaceFirst(cn, INNER_CLASS_PATTERN, "");
cn = replaceAll(cn, SHORTEN_NAME_PATTERN, "$1");
var mn = replaceAll(methodName, SHORTEN_NAME_PATTERN, "$1");
return cn + '#' + mn;
}
private static String replaceFirst(String input, Pattern regex, String replacement) {
return regex.matcher(input).replaceFirst(replacement);
}
private static String replaceAll(String input, Pattern regex, String replacement) {
return regex.matcher(input).replaceAll(replacement);
}
private String formatTx() {
return formatTxId() + " {" + formatTxName(true) + "}";
}
private String formatTxId() {
return Strings.leftPad(Long.toUnsignedString(txLogId, 36), 6, '0') + options.getIsolationLevel().getTxIdSuffix();
}
private String formatTxName(boolean withContext) {
return name + (logLine != null ? ":" + logLine : "") + (withContext && logContext != null ? "/" + logContext : "");
}
@Override
public TxManagerState getState() {
return this;
}
@Override
public boolean isFirstLevelCache() {
return options.isFirstLevelCache();
}
@Nullable
@Override
public IsolationLevel getIsolationLevel() {
return options.isScan() ? null : options.getIsolationLevel();
}
@Override
public boolean isReadOnly() {
return options.isReadOnly();
}
@Override
public boolean isScan() {
return options.isScan();
}
@Override
public String toString() {
return repository.getClass().getSimpleName();
}
@AllArgsConstructor
private class ScanBuilderImpl implements ScanBuilder {
@With(AccessLevel.PRIVATE)
private final TxOptions.ScanOptions options;
@Override
public ScanBuilder withMaxSize(long maxSize) {
return withOptions(options.withMaxSize(maxSize));
}
@Override
public ScanBuilder withTimeout(Duration timeout) {
return withOptions(options.withTimeout(timeout));
}
@Override
public T run(Supplier supplier) throws RetryableException {
TxOptions txOptions = StdTxManager.this.options
.withScanOptions(options)
.withFirstLevelCache(false);
return StdTxManager.this.withOptions(txOptions).tx(supplier);
}
}
@AllArgsConstructor
private class ReadonlyBuilderImpl implements ReadonlyBuilder {
@With(AccessLevel.PRIVATE)
private final TxOptions options;
@Override
public ReadonlyBuilder withStatementIsolationLevel(IsolationLevel isolationLevel) {
Preconditions.checkArgument(isolationLevel.isReadOnly(),
"readOnly() can only be used with a read-only tx isolation level, but got: %s", isolationLevel);
return withOptions(options.withIsolationLevel(isolationLevel));
}
@Override
public ReadonlyBuilder withFirstLevelCache(boolean firstLevelCache) {
return withOptions(options.withFirstLevelCache(firstLevelCache));
}
@Override
public T run(Supplier supplier) throws RetryableException {
return StdTxManager.this.withOptions(options).tx(supplier);
}
}
private enum SeparatePolicy {
ALLOW,
LOG,
STRICT
}
}