/* * Copyright (C) 2017 Julien Viet * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package io.vertx.sqlclient.impl; import java.util.ArrayDeque; import java.util.Deque; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Promise; import io.vertx.core.VertxException; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.PromiseInternal; import io.vertx.sqlclient.Transaction; import io.vertx.sqlclient.TransactionRollbackException; import io.vertx.sqlclient.impl.command.CommandBase; import io.vertx.sqlclient.impl.command.TxCommand; class TransactionImpl implements Transaction { private static final TxCommand<Void> ROLLBACK = new TxCommand<>(TxCommand.Kind.ROLLBACK, null); private static final TxCommand<Void> COMMIT = new TxCommand<>(TxCommand.Kind.COMMIT, null); private static final int ST_BEGIN = 0; private static final int ST_PENDING = 1; private static final int ST_PROCESSING = 2; private static final int ST_COMPLETED = 3; private final ContextInternal context; private final Connection connection; private Deque<ScheduledCommand<?>> pending = new ArrayDeque<>(); private int status = ST_BEGIN; private final Promise<Void> completion; TransactionImpl(ContextInternal context, Connection connection) { this.context = context; this.connection = connection; this.completion = context.promise(); } static class ScheduledCommand<R> { final CommandBase<R> cmd; final Handler<AsyncResult<R>> handler; ScheduledCommand(CommandBase<R> cmd, Handler<AsyncResult<R>> handler) { this.cmd = cmd; this.handler = handler; } } Future<Transaction> begin() { PromiseInternal<Transaction> promise = context.promise(this::afterBegin); ScheduledCommand<Transaction> b = doQuery(new TxCommand<>(TxCommand.Kind.BEGIN, this), promise); doSchedule(b.cmd, b.handler); return promise.future(); } private <R> void doSchedule(CommandBase<R> cmd, Handler<AsyncResult<R>> handler) { connection.schedule(cmd, context.promise(handler)); } private <R> void wrapAndSchedule(ScheduledCommand<R> scheduled) { CommandBase<R> cmd = scheduled.cmd; if (isComplete(cmd)) { status = ST_COMPLETED; doSchedule(cmd, ar -> { if (ar.succeeded()) { if (cmd == COMMIT) { completion.tryComplete(); } else { completion.tryFail(TransactionRollbackException.INSTANCE); } } else { completion.tryFail(ar.cause()); } scheduled.handler.handle(ar); }); } else { status = ST_PROCESSING; doSchedule(cmd, wrap(scheduled.handler)); } } private <T> Handler<AsyncResult<T>> wrap(Handler<AsyncResult<T>> handler) { return ar -> { synchronized (TransactionImpl.this) { status = ST_PENDING; if (ar.failed()) { // We won't recover from this so rollback ScheduledCommand<?> c; while ((c = pending.poll()) != null) { c.handler.handle(Future.failedFuture("Rollback exception")); } schedule__(doQuery(ROLLBACK, context.promise(ar2 -> { handler.handle(ar); }))); } else { handler.handle(ar); checkPending(); } } }; } private synchronized void afterBegin(AsyncResult<Transaction> ar) { if (ar.succeeded()) { status = ST_PENDING; } else { status = ST_COMPLETED; } checkPending(); } private static boolean isComplete(CommandBase<?> cmd) { if (cmd instanceof TxCommand) { TxCommand txCmd = (TxCommand) cmd; return txCmd.kind == TxCommand.Kind.COMMIT || txCmd.kind == TxCommand.Kind.ROLLBACK; } return false; } private synchronized void checkPending() { switch (status) { case ST_BEGIN: break; case ST_PENDING: { ScheduledCommand<?> cmd = pending.poll(); if (cmd != null) { wrapAndSchedule(cmd); } break; } case ST_PROCESSING: break; case ST_COMPLETED: { if (pending.size() > 0) { VertxException err = new VertxException("Transaction already completed"); ScheduledCommand<?> cmd; while ((cmd = pending.poll()) != null) { cmd.cmd.fail(err); } } break; } } } public <R> void schedule(CommandBase<R> cmd, Promise<R> handler) { schedule__(cmd, handler); } public <R> void schedule__(ScheduledCommand<R> b) { synchronized (this) { pending.add(b); } checkPending(); } public <R> void schedule__(CommandBase<R> cmd, Handler<AsyncResult<R>> handler) { schedule__(new ScheduledCommand<>(cmd, handler)); } @Override public Future<Void> commit() { switch (status) { case ST_BEGIN: case ST_PENDING: case ST_PROCESSING: Promise<Void> promise = context.promise(); schedule__(doQuery(COMMIT, promise)); return promise.future(); case ST_COMPLETED: return context.failedFuture("Transaction already completed"); default: throw new IllegalStateException(); } } public void commit(Handler<AsyncResult<Void>> handler) { Future<Void> fut = commit(); if (handler != null) { fut.onComplete(handler); } } @Override public Future<Void> rollback() { if (status == ST_COMPLETED) { return context.failedFuture("Transaction already completed"); } else { Promise<Void> promise = context.promise(); schedule__(doQuery(ROLLBACK, promise)); return promise.future(); } } public void rollback(Handler<AsyncResult<Void>> handler) { Future<Void> fut = rollback(); if (handler != null) { fut.onComplete(handler); } } private <R> ScheduledCommand<R> doQuery(TxCommand<R> cmd, Promise<R> handler) { return new ScheduledCommand<>(cmd, handler); } @Override public Future<Void> completion() { return completion.future(); } }