summaryrefslogtreecommitdiffstats
path: root/sql/xa.cc
diff options
context:
space:
mode:
Diffstat (limited to 'sql/xa.cc')
-rw-r--r--sql/xa.cc1171
1 files changed, 1171 insertions, 0 deletions
diff --git a/sql/xa.cc b/sql/xa.cc
new file mode 100644
index 00000000..49be7fb5
--- /dev/null
+++ b/sql/xa.cc
@@ -0,0 +1,1171 @@
+/*
+ Copyright (c) 2000, 2016, Oracle and/or its affiliates.
+ Copyright (c) 2009, 2020, MariaDB Corporation.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; version 2 of the License.
+
+ 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 General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+*/
+
+#include "mariadb.h"
+#include "sql_class.h"
+#include "transaction.h"
+#include "my_cpu.h"
+#include <pfs_transaction_provider.h>
+#include <mysql/psi/mysql_transaction.h>
+
+static bool slave_applier_reset_xa_trans(THD *thd);
+
+/***************************************************************************
+ Handling of XA id caching
+***************************************************************************/
+struct XID_cache_insert_element
+{
+ enum xa_states xa_state;
+ XID *xid;
+ XID_cache_element *xid_cache_element;
+
+ XID_cache_insert_element(enum xa_states xa_state_arg, XID *xid_arg):
+ xa_state(xa_state_arg), xid(xid_arg) {}
+};
+
+
+class XID_cache_element
+{
+ /*
+ m_state is used to prevent elements from being deleted while XA RECOVER
+ iterates xid cache and to prevent recovered elments from being acquired by
+ multiple threads.
+
+ bits 1..29 are reference counter
+ bit 30 is RECOVERED flag
+ bit 31 is ACQUIRED flag (thread owns this xid)
+ bit 32 is unused
+
+ Newly allocated and deleted elements have m_state set to 0.
+
+ On lock() m_state is atomically incremented. It also creates load-ACQUIRE
+ memory barrier to make sure m_state is actually updated before furhter
+ memory accesses. Attempting to lock an element that has neither ACQUIRED
+ nor RECOVERED flag set returns failure and further accesses to element
+ memory are forbidden.
+
+ On unlock() m_state is decremented. It also creates store-RELEASE memory
+ barrier to make sure m_state is actually updated after preceding memory
+ accesses.
+
+ ACQUIRED flag is set when thread registers it's xid or when thread acquires
+ recovered xid.
+
+ RECOVERED flag is set for elements found during crash recovery.
+
+ ACQUIRED and RECOVERED flags are cleared before element is deleted from
+ hash in a spin loop, after last reference is released.
+ */
+ std::atomic<int32_t> m_state;
+public:
+ static const int32 ACQUIRED= 1 << 30;
+ static const int32 RECOVERED= 1 << 29;
+ /* Error reported by the Resource Manager (RM) to the Transaction Manager. */
+ uint rm_error;
+ enum xa_states xa_state;
+ XID xid;
+ bool is_set(int32_t flag)
+ { return m_state.load(std::memory_order_relaxed) & flag; }
+ void set(int32_t flag)
+ {
+ DBUG_ASSERT(!is_set(ACQUIRED | RECOVERED));
+ m_state.fetch_add(flag, std::memory_order_relaxed);
+ }
+ bool lock()
+ {
+ int32_t old= m_state.fetch_add(1, std::memory_order_acquire);
+ if (old & (ACQUIRED | RECOVERED))
+ return true;
+ unlock();
+ return false;
+ }
+ void unlock()
+ { m_state.fetch_sub(1, std::memory_order_release); }
+ void mark_uninitialized()
+ {
+ int32_t old= ACQUIRED;
+ while (!m_state.compare_exchange_weak(old, 0,
+ std::memory_order_relaxed,
+ std::memory_order_relaxed))
+ {
+ old&= ACQUIRED | RECOVERED;
+ (void) LF_BACKOFF();
+ }
+ }
+ void acquired_to_recovered()
+ {
+ m_state.fetch_or(RECOVERED, std::memory_order_relaxed);
+ m_state.fetch_and(~ACQUIRED, std::memory_order_release);
+ }
+ bool acquire_recovered()
+ {
+ int32_t old= RECOVERED;
+ while (!m_state.compare_exchange_weak(old, ACQUIRED | RECOVERED,
+ std::memory_order_acquire,
+ std::memory_order_relaxed))
+ {
+ if (!(old & RECOVERED) || (old & ACQUIRED))
+ return false;
+ old= RECOVERED;
+ (void) LF_BACKOFF();
+ }
+ return true;
+ }
+ static void lf_hash_initializer(LF_HASH *hash __attribute__((unused)),
+ XID_cache_element *element,
+ XID_cache_insert_element *new_element)
+ {
+ DBUG_ASSERT(!element->is_set(ACQUIRED | RECOVERED));
+ element->rm_error= 0;
+ element->xa_state= new_element->xa_state;
+ element->xid.set(new_element->xid);
+ new_element->xid_cache_element= element;
+ }
+ static void lf_alloc_constructor(uchar *ptr)
+ {
+ XID_cache_element *element= (XID_cache_element*) (ptr + LF_HASH_OVERHEAD);
+ element->m_state= 0;
+ }
+ static void lf_alloc_destructor(uchar *ptr)
+ {
+ DBUG_ASSERT(!reinterpret_cast<XID_cache_element*>(ptr + LF_HASH_OVERHEAD)
+ ->is_set(ACQUIRED));
+ }
+ static uchar *key(const XID_cache_element *element, size_t *length,
+ my_bool not_used __attribute__((unused)))
+ {
+ *length= element->xid.key_length();
+ return element->xid.key();
+ }
+};
+
+
+static LF_HASH xid_cache;
+static bool xid_cache_inited;
+
+
+enum xa_states XID_STATE::get_state_code() const
+{
+ return xid_cache_element ? xid_cache_element->xa_state : XA_NO_STATE;
+}
+
+
+bool THD::fix_xid_hash_pins()
+{
+ if (!xid_hash_pins)
+ xid_hash_pins= lf_hash_get_pins(&xid_cache);
+ return !xid_hash_pins;
+}
+
+
+void XID_STATE::set_error(uint error)
+{
+ if (is_explicit_XA())
+ xid_cache_element->rm_error= error;
+}
+
+
+void XID_STATE::er_xaer_rmfail() const
+{
+ static const char *xa_state_names[]=
+ { "ACTIVE", "IDLE", "PREPARED", "ROLLBACK ONLY", "NON-EXISTING"};
+ my_error(ER_XAER_RMFAIL, MYF(0), xa_state_names[get_state_code()]);
+}
+
+
+/**
+ Check that XA transaction has an uncommitted work. Report an error
+ to the user in case when there is an uncommitted work for XA transaction.
+
+ @return result of check
+ @retval false XA transaction is NOT in state IDLE, PREPARED
+ or ROLLBACK_ONLY.
+ @retval true XA transaction is in state IDLE or PREPARED
+ or ROLLBACK_ONLY.
+*/
+
+bool XID_STATE::check_has_uncommitted_xa() const
+{
+ if (is_explicit_XA() && xid_cache_element->xa_state != XA_ACTIVE)
+ {
+ er_xaer_rmfail();
+ return true;
+ }
+ return false;
+}
+
+
+XID *XID_STATE::get_xid() const
+{
+ DBUG_ASSERT(is_explicit_XA());
+ return &xid_cache_element->xid;
+}
+
+
+void xid_cache_init()
+{
+ xid_cache_inited= true;
+ lf_hash_init(&xid_cache, sizeof(XID_cache_element), LF_HASH_UNIQUE, 0, 0,
+ (my_hash_get_key) XID_cache_element::key, &my_charset_bin);
+ xid_cache.alloc.constructor= XID_cache_element::lf_alloc_constructor;
+ xid_cache.alloc.destructor= XID_cache_element::lf_alloc_destructor;
+ xid_cache.initializer=
+ (lf_hash_initializer) XID_cache_element::lf_hash_initializer;
+}
+
+
+void xid_cache_free()
+{
+ if (xid_cache_inited)
+ {
+ lf_hash_destroy(&xid_cache);
+ xid_cache_inited= false;
+ }
+}
+
+
+/**
+ Find recovered XA transaction by XID.
+*/
+
+static XID_cache_element *xid_cache_search(THD *thd, XID *xid)
+{
+ DBUG_ASSERT(thd->xid_hash_pins);
+ XID_cache_element *element=
+ (XID_cache_element*) lf_hash_search(&xid_cache, thd->xid_hash_pins,
+ xid->key(), xid->key_length());
+ if (element)
+ {
+ /* The element can be removed from lf_hash by other thread, but
+ element->acquire_recovered() will return false in this case. */
+ if (!element->acquire_recovered())
+ element= 0;
+ lf_hash_search_unpin(thd->xid_hash_pins);
+ /* Once the element is acquired (i.e. got the ACQUIRED bit) by this thread,
+ only this thread can delete it. The deletion happens in xid_cache_delete().
+ See also the XID_cache_element documentation. */
+ DEBUG_SYNC(thd, "xa_after_search");
+ }
+ return element;
+}
+
+
+bool xid_cache_insert(XID *xid)
+{
+ XID_cache_insert_element new_element(XA_PREPARED, xid);
+ LF_PINS *pins;
+
+ if (!(pins= lf_hash_get_pins(&xid_cache)))
+ return true;
+
+ int res= lf_hash_insert(&xid_cache, pins, &new_element);
+ switch (res)
+ {
+ case 0:
+ new_element.xid_cache_element->set(XID_cache_element::RECOVERED);
+ break;
+ case 1:
+ res= 0;
+ }
+ lf_hash_put_pins(pins);
+ return res;
+}
+
+
+bool xid_cache_insert(THD *thd, XID_STATE *xid_state, XID *xid)
+{
+ XID_cache_insert_element new_element(XA_ACTIVE, xid);
+
+ if (thd->fix_xid_hash_pins())
+ return true;
+
+ int res= lf_hash_insert(&xid_cache, thd->xid_hash_pins, &new_element);
+ switch (res)
+ {
+ case 0:
+ xid_state->xid_cache_element= new_element.xid_cache_element;
+ xid_state->xid_cache_element->set(XID_cache_element::ACQUIRED);
+ break;
+ case 1:
+ my_error(ER_XAER_DUPID, MYF(0));
+ }
+ return res;
+}
+
+
+static void xid_cache_delete(THD *thd, XID_cache_element *&element)
+{
+ DBUG_ASSERT(thd->xid_hash_pins);
+ element->mark_uninitialized();
+ lf_hash_delete(&xid_cache, thd->xid_hash_pins,
+ element->xid.key(), element->xid.key_length());
+}
+
+
+void xid_cache_delete(THD *thd, XID_STATE *xid_state)
+{
+ DBUG_ASSERT(xid_state->is_explicit_XA());
+ xid_cache_delete(thd, xid_state->xid_cache_element);
+ xid_state->xid_cache_element= 0;
+}
+
+
+struct xid_cache_iterate_arg
+{
+ my_hash_walk_action action;
+ void *argument;
+};
+
+static my_bool xid_cache_iterate_callback(XID_cache_element *element,
+ xid_cache_iterate_arg *arg)
+{
+ my_bool res= FALSE;
+ if (element->lock())
+ {
+ res= arg->action(element, arg->argument);
+ element->unlock();
+ }
+ return res;
+}
+
+static int xid_cache_iterate(THD *thd, my_hash_walk_action action, void *arg)
+{
+ xid_cache_iterate_arg argument= { action, arg };
+ return thd->fix_xid_hash_pins() ? -1 :
+ lf_hash_iterate(&xid_cache, thd->xid_hash_pins,
+ (my_hash_walk_action) xid_cache_iterate_callback,
+ &argument);
+}
+
+
+/**
+ Mark a XA transaction as rollback-only if the RM unilaterally
+ rolled back the transaction branch.
+
+ @note If a rollback was requested by the RM, this function sets
+ the appropriate rollback error code and transits the state
+ to XA_ROLLBACK_ONLY.
+
+ @return TRUE if transaction was rolled back or if the transaction
+ state is XA_ROLLBACK_ONLY. FALSE otherwise.
+*/
+static bool xa_trans_rolled_back(XID_cache_element *element)
+{
+ if (element->rm_error)
+ {
+ switch (element->rm_error) {
+ case ER_LOCK_WAIT_TIMEOUT:
+ my_error(ER_XA_RBTIMEOUT, MYF(0));
+ break;
+ case ER_LOCK_DEADLOCK:
+ my_error(ER_XA_RBDEADLOCK, MYF(0));
+ break;
+ default:
+ my_error(ER_XA_RBROLLBACK, MYF(0));
+ }
+ element->xa_state= XA_ROLLBACK_ONLY;
+ }
+
+ return element->xa_state == XA_ROLLBACK_ONLY;
+}
+
+
+/**
+ Rollback the active XA transaction.
+
+ @return TRUE if the rollback failed, FALSE otherwise.
+*/
+
+bool xa_trans_force_rollback(THD *thd)
+{
+ bool rc= false;
+
+ if (ha_rollback_trans(thd, true))
+ {
+ my_error(ER_XAER_RMERR, MYF(0));
+ rc= true;
+ }
+ thd->variables.option_bits&=
+ ~(OPTION_BEGIN | OPTION_BINLOG_THIS_TRX | OPTION_GTID_BEGIN);
+ thd->transaction->all.reset();
+ thd->server_status&=
+ ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
+ DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
+ xid_cache_delete(thd, &thd->transaction->xid_state);
+
+ trans_track_end_trx(thd);
+ thd->mdl_context.release_transactional_locks(thd);
+
+ return rc;
+}
+
+
+/**
+ Starts an XA transaction with the given xid value.
+
+ @param thd Current thread
+
+ @retval FALSE Success
+ @retval TRUE Failure
+*/
+
+bool trans_xa_start(THD *thd)
+{
+ DBUG_ENTER("trans_xa_start");
+
+ if (thd->transaction->xid_state.is_explicit_XA() &&
+ thd->transaction->xid_state.xid_cache_element->xa_state == XA_IDLE &&
+ thd->lex->xa_opt == XA_RESUME)
+ {
+ bool not_equal=
+ !thd->transaction->xid_state.xid_cache_element->xid.eq(thd->lex->xid);
+ if (not_equal)
+ my_error(ER_XAER_NOTA, MYF(0));
+ else
+ {
+ thd->transaction->xid_state.xid_cache_element->xa_state= XA_ACTIVE;
+ MYSQL_SET_TRANSACTION_XA_STATE(thd->m_transaction_psi, XA_ACTIVE);
+ }
+ DBUG_RETURN(not_equal);
+ }
+
+ /* TODO: JOIN is not supported yet. */
+ if (thd->lex->xa_opt != XA_NONE)
+ my_error(ER_XAER_INVAL, MYF(0));
+ else if (!thd->lex->xid->gtrid_length)
+ my_error(ER_XAER_INVAL, MYF(0));
+ else if (thd->transaction->xid_state.is_explicit_XA())
+ thd->transaction->xid_state.er_xaer_rmfail();
+ else if (thd->locked_tables_mode || thd->in_active_multi_stmt_transaction())
+ my_error(ER_XAER_OUTSIDE, MYF(0));
+ else if (!trans_begin(thd))
+ {
+ MYSQL_SET_TRANSACTION_XID(thd->m_transaction_psi, thd->lex->xid, XA_ACTIVE);
+ if (xid_cache_insert(thd, &thd->transaction->xid_state, thd->lex->xid))
+ {
+ trans_rollback(thd);
+ DBUG_RETURN(true);
+ }
+ DBUG_RETURN(FALSE);
+ }
+
+ DBUG_RETURN(TRUE);
+}
+
+
+/**
+ Put a XA transaction in the IDLE state.
+
+ @param thd Current thread
+
+ @retval FALSE Success
+ @retval TRUE Failure
+*/
+
+bool trans_xa_end(THD *thd)
+{
+ DBUG_ENTER("trans_xa_end");
+
+ /* TODO: SUSPEND and FOR MIGRATE are not supported yet. */
+ if (thd->lex->xa_opt != XA_NONE)
+ my_error(ER_XAER_INVAL, MYF(0));
+ else if (!thd->transaction->xid_state.is_explicit_XA() ||
+ thd->transaction->xid_state.xid_cache_element->xa_state != XA_ACTIVE)
+ thd->transaction->xid_state.er_xaer_rmfail();
+ else if (!thd->transaction->xid_state.xid_cache_element->xid.eq(thd->lex->xid))
+ my_error(ER_XAER_NOTA, MYF(0));
+ else if (!xa_trans_rolled_back(thd->transaction->xid_state.xid_cache_element))
+ {
+ thd->transaction->xid_state.xid_cache_element->xa_state= XA_IDLE;
+ MYSQL_SET_TRANSACTION_XA_STATE(thd->m_transaction_psi, XA_IDLE);
+ }
+
+ DBUG_RETURN(thd->is_error() ||
+ thd->transaction->xid_state.xid_cache_element->xa_state != XA_IDLE);
+}
+
+
+/**
+ Put a XA transaction in the PREPARED state.
+
+ @param thd Current thread
+
+ @retval FALSE Success
+ @retval TRUE Failure
+*/
+
+bool trans_xa_prepare(THD *thd)
+{
+ int res= 1;
+
+ DBUG_ENTER("trans_xa_prepare");
+
+ if (!thd->transaction->xid_state.is_explicit_XA() ||
+ thd->transaction->xid_state.xid_cache_element->xa_state != XA_IDLE)
+ thd->transaction->xid_state.er_xaer_rmfail();
+ else if (!thd->transaction->xid_state.xid_cache_element->xid.eq(thd->lex->xid))
+ my_error(ER_XAER_NOTA, MYF(0));
+ else
+ {
+ /*
+ Acquire metadata lock which will ensure that COMMIT is blocked
+ by active FLUSH TABLES WITH READ LOCK (and vice versa COMMIT in
+ progress blocks FTWRL).
+
+ We allow FLUSHer to COMMIT; we assume FLUSHer knows what it does.
+ */
+ MDL_request mdl_request;
+ MDL_REQUEST_INIT(&mdl_request, MDL_key::BACKUP, "", "", MDL_BACKUP_COMMIT,
+ MDL_STATEMENT);
+ if (thd->mdl_context.acquire_lock(&mdl_request,
+ thd->variables.lock_wait_timeout) ||
+ ha_prepare(thd))
+ {
+ if (!mdl_request.ticket)
+ ha_rollback_trans(thd, TRUE);
+ thd->variables.option_bits&= ~(OPTION_BEGIN | OPTION_BINLOG_THIS_TRX);
+ thd->transaction->all.reset();
+ thd->server_status&=
+ ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
+ xid_cache_delete(thd, &thd->transaction->xid_state);
+ my_error(ER_XA_RBROLLBACK, MYF(0));
+ }
+ else
+ {
+ thd->transaction->xid_state.xid_cache_element->xa_state= XA_PREPARED;
+ MYSQL_SET_TRANSACTION_XA_STATE(thd->m_transaction_psi, XA_PREPARED);
+ res= thd->variables.pseudo_slave_mode || thd->slave_thread ?
+ slave_applier_reset_xa_trans(thd) : 0;
+ }
+ }
+
+ DBUG_RETURN(res);
+}
+
+
+/**
+ Commit and terminate the a XA transaction.
+ Transactional locks are released if transaction ended
+
+ @param thd Current thread
+
+ @retval FALSE Success
+ @retval TRUE Failure
+
+*/
+
+bool trans_xa_commit(THD *thd)
+{
+ bool res= true;
+ XID_STATE &xid_state= thd->transaction->xid_state;
+
+ DBUG_ENTER("trans_xa_commit");
+
+ if (!xid_state.is_explicit_XA() ||
+ !xid_state.xid_cache_element->xid.eq(thd->lex->xid))
+ {
+ if (thd->in_multi_stmt_transaction_mode())
+ {
+ /*
+ Not allow to commit from inside an not-"native" to xid
+ ongoing transaction: the commit effect can't be reversed.
+ */
+ my_error(ER_XAER_OUTSIDE, MYF(0));
+ DBUG_RETURN(TRUE);
+ }
+ if (thd->lex->xa_opt != XA_NONE)
+ {
+ /*
+ Not allow to commit with one phase a prepared xa out of compatibility
+ with the native commit branch's error out.
+ */
+ my_error(ER_XAER_INVAL, MYF(0));
+ DBUG_RETURN(TRUE);
+ }
+ if (thd->fix_xid_hash_pins())
+ {
+ my_error(ER_OUT_OF_RESOURCES, MYF(0));
+ DBUG_RETURN(TRUE);
+ }
+
+ if (auto xs= xid_cache_search(thd, thd->lex->xid))
+ {
+ bool xid_deleted= false;
+ MDL_request mdl_request;
+ bool rw_trans= (xs->rm_error != ER_XA_RBROLLBACK);
+
+ if (rw_trans && thd->is_read_only_ctx())
+ {
+ my_error(ER_OPTION_PREVENTS_STATEMENT, MYF(0), "--read-only");
+ res= 1;
+ goto _end_external_xid;
+ }
+
+ res= xa_trans_rolled_back(xs);
+ /*
+ Acquire metadata lock which will ensure that COMMIT is blocked
+ by active FLUSH TABLES WITH READ LOCK (and vice versa COMMIT in
+ progress blocks FTWRL).
+
+ We allow FLUSHer to COMMIT; we assume FLUSHer knows what it does.
+ */
+ MDL_REQUEST_INIT(&mdl_request, MDL_key::BACKUP, "", "", MDL_BACKUP_COMMIT,
+ MDL_EXPLICIT);
+ if (thd->mdl_context.acquire_lock(&mdl_request,
+ thd->variables.lock_wait_timeout))
+ {
+ /*
+ We can't rollback an XA transaction on lock failure due to
+ Innodb redo log and bin log update is involved in rollback.
+ Return error to user for a retry.
+ */
+ DBUG_ASSERT(thd->is_error());
+
+ res= true;
+ goto _end_external_xid;
+ }
+ else
+ {
+ thd->backup_commit_lock= &mdl_request;
+ }
+ DBUG_ASSERT(!xid_state.xid_cache_element);
+
+ xid_state.xid_cache_element= xs;
+ ha_commit_or_rollback_by_xid(thd->lex->xid, !res);
+ if (!res && thd->is_error())
+ {
+ // hton completion error retains xs/xid in the cache,
+ // unless there had been already one as reflected by `res`.
+ res= true;
+ goto _end_external_xid;
+ }
+ xid_cache_delete(thd, xs);
+ xid_deleted= true;
+
+ _end_external_xid:
+ xid_state.xid_cache_element= 0;
+ res= res || thd->is_error();
+ if (!xid_deleted)
+ xs->acquired_to_recovered();
+ if (mdl_request.ticket)
+ {
+ thd->mdl_context.release_lock(mdl_request.ticket);
+ thd->backup_commit_lock= 0;
+ }
+ }
+ else
+ my_error(ER_XAER_NOTA, MYF(0));
+ DBUG_RETURN(res);
+ }
+
+ if (thd->transaction->all.is_trx_read_write() && thd->is_read_only_ctx())
+ {
+ my_error(ER_OPTION_PREVENTS_STATEMENT, MYF(0), "--read-only");
+ DBUG_RETURN(TRUE);
+ } else if (xa_trans_rolled_back(xid_state.xid_cache_element))
+ {
+ xa_trans_force_rollback(thd);
+ DBUG_RETURN(thd->is_error());
+ }
+ else if (xid_state.xid_cache_element->xa_state == XA_IDLE &&
+ thd->lex->xa_opt == XA_ONE_PHASE)
+ {
+ int r= ha_commit_trans(thd, TRUE);
+ if ((res= MY_TEST(r)))
+ my_error(r == 1 ? ER_XA_RBROLLBACK : ER_XAER_RMERR, MYF(0));
+ }
+ else if (thd->transaction->xid_state.xid_cache_element->xa_state == XA_PREPARED)
+ {
+ MDL_request mdl_request;
+ if (thd->lex->xa_opt != XA_NONE)
+ {
+ my_error(ER_XAER_INVAL, MYF(0));
+ DBUG_RETURN(TRUE);
+ }
+
+ /*
+ Acquire metadata lock which will ensure that COMMIT is blocked
+ by active FLUSH TABLES WITH READ LOCK (and vice versa COMMIT in
+ progress blocks FTWRL).
+
+ We allow FLUSHer to COMMIT; we assume FLUSHer knows what it does.
+ */
+ MDL_REQUEST_INIT(&mdl_request, MDL_key::BACKUP, "", "", MDL_BACKUP_COMMIT,
+ MDL_TRANSACTION);
+
+ if (thd->mdl_context.acquire_lock(&mdl_request,
+ thd->variables.lock_wait_timeout))
+ {
+ /*
+ We can't rollback an XA transaction on lock failure due to
+ Innodb redo log and bin log update is involved in rollback.
+ Return error to user for a retry.
+ */
+ my_error(ER_XAER_RMERR, MYF(0));
+ DBUG_RETURN(true);
+ }
+ else
+ {
+ DEBUG_SYNC(thd, "trans_xa_commit_after_acquire_commit_lock");
+
+ res= MY_TEST(ha_commit_one_phase(thd, 1));
+ if (res)
+ my_error(ER_XAER_RMERR, MYF(0));
+ else
+ {
+ /*
+ Since we don't call ha_commit_trans() for prepared transactions,
+ we need to explicitly mark the transaction as committed.
+ */
+ MYSQL_COMMIT_TRANSACTION(thd->m_transaction_psi);
+ }
+
+ thd->m_transaction_psi= NULL;
+ }
+ }
+ else
+ {
+ xid_state.er_xaer_rmfail();
+ DBUG_RETURN(TRUE);
+ }
+
+ thd->variables.option_bits&= ~(OPTION_BEGIN | OPTION_BINLOG_THIS_TRX);
+ thd->transaction->all.reset();
+ thd->server_status&=
+ ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
+ DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
+ xid_cache_delete(thd, &xid_state);
+
+ trans_track_end_trx(thd);
+ thd->mdl_context.release_transactional_locks(thd);
+
+ /* The transaction should be marked as complete in P_S. */
+ DBUG_ASSERT(thd->m_transaction_psi == NULL || res);
+ DBUG_RETURN(res);
+}
+
+
+/**
+ Roll back and terminate a XA transaction.
+ Transactional locks are released if transaction ended
+
+ @param thd Current thread
+
+ @retval FALSE Success
+ @retval TRUE Failure
+*/
+
+bool trans_xa_rollback(THD *thd)
+{
+ XID_STATE &xid_state= thd->transaction->xid_state;
+
+ DBUG_ENTER("trans_xa_rollback");
+
+ if (!xid_state.is_explicit_XA() ||
+ !xid_state.xid_cache_element->xid.eq(thd->lex->xid))
+ {
+ if (thd->in_multi_stmt_transaction_mode())
+ {
+ my_error(ER_XAER_OUTSIDE, MYF(0));
+ DBUG_RETURN(TRUE);
+ }
+ if (thd->fix_xid_hash_pins())
+ {
+ my_error(ER_OUT_OF_RESOURCES, MYF(0));
+ DBUG_RETURN(TRUE);
+ }
+
+ if (auto xs= xid_cache_search(thd, thd->lex->xid))
+ {
+ bool res;
+ bool xid_deleted= false;
+ MDL_request mdl_request;
+ bool rw_trans= (xs->rm_error != ER_XA_RBROLLBACK);
+
+ if (rw_trans && thd->is_read_only_ctx())
+ {
+ my_error(ER_OPTION_PREVENTS_STATEMENT, MYF(0), "--read-only");
+ res= 1;
+ goto _end_external_xid;
+ }
+
+ MDL_REQUEST_INIT(&mdl_request, MDL_key::BACKUP, "", "", MDL_BACKUP_COMMIT,
+ MDL_EXPLICIT);
+ if (thd->mdl_context.acquire_lock(&mdl_request,
+ thd->variables.lock_wait_timeout))
+ {
+ /*
+ We can't rollback an XA transaction on lock failure due to
+ Innodb redo log and bin log update is involved in rollback.
+ Return error to user for a retry.
+ */
+ DBUG_ASSERT(thd->is_error());
+
+ goto _end_external_xid;
+ }
+ else
+ {
+ thd->backup_commit_lock= &mdl_request;
+ }
+ res= xa_trans_rolled_back(xs);
+ DBUG_ASSERT(!xid_state.xid_cache_element);
+
+ xid_state.xid_cache_element= xs;
+ ha_commit_or_rollback_by_xid(thd->lex->xid, 0);
+ if (!res && thd->is_error())
+ {
+ goto _end_external_xid;
+ }
+ xid_cache_delete(thd, xs);
+ xid_deleted= true;
+
+ _end_external_xid:
+ xid_state.xid_cache_element= 0;
+ if (!xid_deleted)
+ xs->acquired_to_recovered();
+ if (mdl_request.ticket)
+ {
+ thd->mdl_context.release_lock(mdl_request.ticket);
+ thd->backup_commit_lock= 0;
+ }
+ }
+ else
+ my_error(ER_XAER_NOTA, MYF(0));
+ DBUG_RETURN(thd->get_stmt_da()->is_error());
+ }
+
+ if (thd->transaction->all.is_trx_read_write() && thd->is_read_only_ctx())
+ {
+ my_error(ER_OPTION_PREVENTS_STATEMENT, MYF(0), "--read-only");
+ DBUG_RETURN(TRUE);
+ } else if (xid_state.xid_cache_element->xa_state == XA_ACTIVE)
+ {
+ xid_state.er_xaer_rmfail();
+ DBUG_RETURN(TRUE);
+ }
+
+ MDL_request mdl_request;
+ MDL_REQUEST_INIT(&mdl_request, MDL_key::BACKUP, "", "", MDL_BACKUP_COMMIT,
+ MDL_STATEMENT);
+ if (thd->mdl_context.acquire_lock(&mdl_request,
+ thd->variables.lock_wait_timeout))
+ {
+ /*
+ We can't rollback an XA transaction on lock failure due to
+ Innodb redo log and bin log update is involved in rollback.
+ Return error to user for a retry.
+ */
+ my_error(ER_XAER_RMERR, MYF(0));
+ DBUG_RETURN(true);
+ }
+
+ DBUG_RETURN(xa_trans_force_rollback(thd));
+}
+
+
+bool trans_xa_detach(THD *thd)
+{
+ DBUG_ASSERT(thd->transaction->xid_state.is_explicit_XA());
+
+ if (thd->transaction->xid_state.xid_cache_element->xa_state != XA_PREPARED)
+ return xa_trans_force_rollback(thd);
+ else if (!thd->transaction->all.is_trx_read_write())
+ {
+ thd->transaction->xid_state.set_error(ER_XA_RBROLLBACK);
+ ha_rollback_trans(thd, true);
+ }
+
+ thd->transaction->xid_state.xid_cache_element->acquired_to_recovered();
+ thd->transaction->xid_state.xid_cache_element= 0;
+ thd->transaction->cleanup();
+
+ Ha_trx_info *ha_info, *ha_info_next;
+ for (ha_info= thd->transaction->all.ha_list;
+ ha_info;
+ ha_info= ha_info_next)
+ {
+ ha_info_next= ha_info->next();
+ ha_info->reset(); /* keep it conveniently zero-filled */
+ }
+
+ thd->transaction->all.ha_list= 0;
+ thd->transaction->all.no_2pc= 0;
+ thd->m_transaction_psi= 0;
+ thd->server_status&= ~(SERVER_STATUS_IN_TRANS |
+ SERVER_STATUS_IN_TRANS_READONLY);
+ thd->mdl_context.release_transactional_locks(thd);
+
+ return false;
+}
+
+
+/**
+ return the XID as it appears in the SQL function's arguments.
+ So this string can be passed to XA START, XA PREPARE etc...
+
+ @note
+ the 'buf' has to have space for at least SQL_XIDSIZE bytes.
+*/
+
+
+/*
+ 'a'..'z' 'A'..'Z', '0'..'9'
+ and '-' '_' ' ' symbols don't have to be
+ converted.
+*/
+
+static const char xid_needs_conv[128]=
+{
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,
+ 0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,
+ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,
+ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1
+};
+
+/*
+ The size of XID string representation in the form
+ 'gtrid', 'bqual', formatID
+ see xid_t::get_sql_string() for details.
+*/
+#define SQL_XIDSIZE (XIDDATASIZE * 2 + 8 + MY_INT64_NUM_DECIMAL_DIGITS)
+
+/* The 'buf' has to have space for at least SQL_XIDSIZE bytes. */
+static uint get_sql_xid(XID *xid, char *buf)
+{
+ int tot_len= xid->gtrid_length + xid->bqual_length;
+ int i;
+ const char *orig_buf= buf;
+
+ for (i=0; i<tot_len; i++)
+ {
+ uchar c= ((uchar *) xid->data)[i];
+ if (c >= 128 || xid_needs_conv[c])
+ break;
+ }
+
+ if (i >= tot_len)
+ {
+ /* No need to convert characters to hexadecimals. */
+ *buf++= '\'';
+ memcpy(buf, xid->data, xid->gtrid_length);
+ buf+= xid->gtrid_length;
+ *buf++= '\'';
+ if (xid->bqual_length > 0 || xid->formatID != 1)
+ {
+ *buf++= ',';
+ *buf++= '\'';
+ memcpy(buf, xid->data+xid->gtrid_length, xid->bqual_length);
+ buf+= xid->bqual_length;
+ *buf++= '\'';
+ }
+ }
+ else
+ {
+ *buf++= 'X';
+ *buf++= '\'';
+ for (i= 0; i < xid->gtrid_length; i++)
+ {
+ *buf++=_dig_vec_lower[((uchar*) xid->data)[i] >> 4];
+ *buf++=_dig_vec_lower[((uchar*) xid->data)[i] & 0x0f];
+ }
+ *buf++= '\'';
+ if (xid->bqual_length > 0 || xid->formatID != 1)
+ {
+ *buf++= ',';
+ *buf++= 'X';
+ *buf++= '\'';
+ for (; i < tot_len; i++)
+ {
+ *buf++=_dig_vec_lower[((uchar*) xid->data)[i] >> 4];
+ *buf++=_dig_vec_lower[((uchar*) xid->data)[i] & 0x0f];
+ }
+ *buf++= '\'';
+ }
+ }
+
+ if (xid->formatID != 1)
+ {
+ *buf++= ',';
+ buf+= my_longlong10_to_str_8bit(&my_charset_bin, buf,
+ MY_INT64_NUM_DECIMAL_DIGITS, -10, xid->formatID);
+ }
+
+ return (uint)(buf - orig_buf);
+}
+
+
+/**
+ return the list of XID's to a client, the same way SHOW commands do.
+
+ @note
+ I didn't find in XA specs that an RM cannot return the same XID twice,
+ so mysql_xa_recover does not filter XID's to ensure uniqueness.
+ It can be easily fixed later, if necessary.
+*/
+
+static my_bool xa_recover_callback(XID_cache_element *xs, Protocol *protocol,
+ char *data, uint data_len, CHARSET_INFO *data_cs)
+{
+ if (xs->xa_state == XA_PREPARED)
+ {
+ protocol->prepare_for_resend();
+ protocol->store_longlong((longlong) xs->xid.formatID, FALSE);
+ protocol->store_longlong((longlong) xs->xid.gtrid_length, FALSE);
+ protocol->store_longlong((longlong) xs->xid.bqual_length, FALSE);
+ protocol->store(data, data_len, data_cs);
+ if (protocol->write())
+ return TRUE;
+ }
+ return FALSE;
+}
+
+
+static my_bool xa_recover_callback_short(XID_cache_element *xs,
+ Protocol *protocol)
+{
+ return xa_recover_callback(xs, protocol, xs->xid.data,
+ xs->xid.gtrid_length + xs->xid.bqual_length, &my_charset_bin);
+}
+
+
+static my_bool xa_recover_callback_verbose(XID_cache_element *xs,
+ Protocol *protocol)
+{
+ char buf[SQL_XIDSIZE];
+ uint len= get_sql_xid(&xs->xid, buf);
+ return xa_recover_callback(xs, protocol, buf, len,
+ &my_charset_utf8mb3_general_ci);
+}
+
+
+/**
+ Collect field names of result set that will be sent to a client in result of
+ handling XA RECOVER statement.
+
+ @param thd Thread data object
+ @param[out] fields List of fields whose metadata should be collected for
+ sending to client
+*/
+
+void xa_recover_get_fields(THD *thd, List<Item> *field_list,
+ my_hash_walk_action *action)
+{
+ MEM_ROOT *mem_root= thd->mem_root;
+
+ field_list->push_back(new (mem_root)
+ Item_int(thd, "formatID", 0,
+ MY_INT32_NUM_DECIMAL_DIGITS), mem_root);
+ field_list->push_back(new (mem_root)
+ Item_int(thd, "gtrid_length", 0,
+ MY_INT32_NUM_DECIMAL_DIGITS), mem_root);
+ field_list->push_back(new (mem_root)
+ Item_int(thd, "bqual_length", 0,
+ MY_INT32_NUM_DECIMAL_DIGITS), mem_root);
+ {
+ uint len;
+ CHARSET_INFO *cs;
+
+ if (thd->lex->verbose)
+ {
+ len= SQL_XIDSIZE;
+ cs= &my_charset_utf8mb3_general_ci;
+ if (action)
+ *action= (my_hash_walk_action) xa_recover_callback_verbose;
+ }
+ else
+ {
+ len= XIDDATASIZE;
+ cs= &my_charset_bin;
+ if (action)
+ *action= (my_hash_walk_action) xa_recover_callback_short;
+ }
+
+ field_list->push_back(new (mem_root)
+ Item_empty_string(thd, "data", len, cs), mem_root);
+ }
+}
+
+bool mysql_xa_recover(THD *thd)
+{
+ List<Item> field_list;
+ Protocol *protocol= thd->protocol;
+ my_hash_walk_action action;
+ DBUG_ENTER("mysql_xa_recover");
+
+ xa_recover_get_fields(thd, &field_list, &action);
+
+ if (protocol->send_result_set_metadata(&field_list,
+ Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
+ DBUG_RETURN(1);
+
+ if (xid_cache_iterate(thd, action, protocol))
+ DBUG_RETURN(1);
+ my_eof(thd);
+ DBUG_RETURN(0);
+}
+
+
+/**
+ This is a specific to (pseudo-) slave applier collection of standard cleanup
+ actions to reset XA transaction state sim to @c ha_commit_one_phase.
+ THD of the slave applier is dissociated from a transaction object in engine
+ that continues to exist there.
+
+ @param THD current thread
+ @return the value of is_error()
+*/
+
+static bool slave_applier_reset_xa_trans(THD *thd)
+{
+ thd->variables.option_bits&= ~(OPTION_BEGIN | OPTION_BINLOG_THIS_TRX);
+ thd->server_status&=
+ ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
+ DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
+
+ if (thd->variables.pseudo_slave_mode &&
+ !thd->transaction->all.is_trx_read_write())
+ {
+ thd->transaction->xid_state.set_error(ER_XA_RBROLLBACK);
+ }
+ thd->transaction->xid_state.xid_cache_element->acquired_to_recovered();
+ thd->transaction->xid_state.xid_cache_element= 0;
+
+ for (Ha_trx_info *ha_info= thd->transaction->all.ha_list, *ha_info_next;
+ ha_info; ha_info= ha_info_next)
+ {
+ ha_info_next= ha_info->next();
+ ha_info->reset();
+ }
+ thd->transaction->all.ha_list= 0;
+
+ ha_close_connection(thd);
+ thd->transaction->cleanup();
+ thd->transaction->all.reset();
+
+ DBUG_ASSERT(!thd->transaction->all.ha_list);
+ DBUG_ASSERT(!thd->transaction->all.no_2pc);
+
+ thd->has_waiter= false;
+ MYSQL_COMMIT_TRANSACTION(thd->m_transaction_psi); // TODO/Fixme: commit?
+ thd->m_transaction_psi= NULL;
+ return thd->is_error();
+}