diff --git a/doc/sql.extensions/README.autonomous_transactions.txt b/doc/sql.extensions/README.autonomous_transactions.txt index 5cbb3d3dc1..872db3f2db 100644 --- a/doc/sql.extensions/README.autonomous_transactions.txt +++ b/doc/sql.extensions/README.autonomous_transactions.txt @@ -10,7 +10,9 @@ need to raise an exception but do not want the database changes to be rolled-bac If exceptions are raised inside the body of an autonomous transaction block, the changes are rolled-back. If the block runs till the end, the transaction is committed. -The new transaction is initiated with snapshot isolation level and lock timeout of the existing one. +The new transaction is initiated with the same isolation level and lock timeout of the existing one. +The only exception is that if existing transaction run in Read Committed Read Consistency mode, then +new autonomous transaction will run in Concurrency mode. Should be used with caution to avoid deadlocks. Author: diff --git a/src/dsql/BlrDebugWriter.cpp b/src/dsql/BlrDebugWriter.cpp index 4757e16cd1..f20040fb92 100644 --- a/src/dsql/BlrDebugWriter.cpp +++ b/src/dsql/BlrDebugWriter.cpp @@ -58,21 +58,9 @@ void BlrDebugWriter::putDebugSrcInfo(ULONG line, ULONG col) { debugData.add(fb_dbg_map_src2blr); - debugData.add(line); - debugData.add(line >> 8); - debugData.add(line >> 16); - debugData.add(line >> 24); - - debugData.add(col); - debugData.add(col >> 8); - debugData.add(col >> 16); - debugData.add(col >> 24); - - const ULONG offset = (getBlrData().getCount() - getBaseOffset()); - debugData.add(offset); - debugData.add(offset >> 8); - debugData.add(offset >> 16); - debugData.add(offset >> 24); + putValue(line); + putValue(col); + putBlrOffset(); } void BlrDebugWriter::putDebugVariable(USHORT number, const MetaName& name) @@ -132,10 +120,7 @@ void BlrDebugWriter::putDebugSubFunction(DeclareSubFuncNode* subFuncNode) HalfStaticArray& subDebugData = subFuncNode->blockScratch->debugData; const ULONG count = ULONG(subDebugData.getCount()); - debugData.add(UCHAR(count)); - debugData.add(UCHAR(count >> 8)); - debugData.add(UCHAR(count >> 16)); - debugData.add(UCHAR(count >> 24)); + putValue(count); debugData.add(subDebugData.begin(), count); } @@ -152,11 +137,29 @@ void BlrDebugWriter::putDebugSubProcedure(DeclareSubProcNode* subProcNode) HalfStaticArray& subDebugData = subProcNode->blockScratch->debugData; const ULONG count = ULONG(subDebugData.getCount()); - debugData.add(UCHAR(count)); - debugData.add(UCHAR(count >> 8)); - debugData.add(UCHAR(count >> 16)); - debugData.add(UCHAR(count >> 24)); + putValue(count); debugData.add(subDebugData.begin(), count); } +void BlrDebugWriter::putDebugMarkers(ULONG marks) +{ + debugData.add(fb_dbg_map_markers); + putValue(marks); + putBlrOffset(); +} + +void BlrDebugWriter::putValue(ULONG val) +{ + debugData.add(val); + debugData.add(val >> 8); + debugData.add(val >> 16); + debugData.add(val >> 24); +} + +void BlrDebugWriter::putBlrOffset() +{ + const ULONG offset = (getBlrData().getCount() - getBaseOffset()); + putValue(offset); +} + } // namespace Jrd diff --git a/src/dsql/BlrDebugWriter.h b/src/dsql/BlrDebugWriter.h index 22ff42c67d..41980861ba 100644 --- a/src/dsql/BlrDebugWriter.h +++ b/src/dsql/BlrDebugWriter.h @@ -50,12 +50,16 @@ public: void putDebugCursor(USHORT, const Firebird::MetaName&); void putDebugSubFunction(DeclareSubFuncNode* subFuncNode); void putDebugSubProcedure(DeclareSubProcNode* subProcNode); + void putDebugMarkers(ULONG marks); DebugData& getDebugData() { return debugData; } virtual void raiseError(const Firebird::Arg::StatusVector& vector); private: + void putValue(ULONG val); + void putBlrOffset(); + DebugData debugData; }; diff --git a/src/dsql/Nodes.h b/src/dsql/Nodes.h index 255953724b..22185f31cf 100644 --- a/src/dsql/Nodes.h +++ b/src/dsql/Nodes.h @@ -1400,6 +1400,12 @@ public: POST_TRIG = 2 }; + // Marks used by EraseNode, ModifyNode and StoreNode + static const unsigned MARK_POSITIONED = 0x01; // Erase|Modify node is positioned at explicit cursor + static const unsigned MARK_MERGE = 0x02; // node is part of MERGE statement + // Marks used by ForNode + static const unsigned MARK_FOR_UPDATE = 0x04; // implicit cursor used in UPDATE\DELETE\MERGE statement + struct ExeState { ExeState(thread_db* tdbb, jrd_req* request, jrd_tra* transaction) diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index 3a4d16a165..fcd01e0310 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -93,6 +93,7 @@ static RelationSourceNode* pass1Update(thread_db* tdbb, CompilerScratch* csb, jr const TrigVector* trigger, StreamType stream, StreamType updateStream, SecurityClass::flags_t priv, jrd_rel* view, StreamType viewStream, StreamType viewUpdateStream); static void pass1Validations(thread_db* tdbb, CompilerScratch* csb, Array& validations); +static ForNode* pass2FindForNode(StmtNode* node, StreamType stream); static void postTriggerAccess(CompilerScratch* csb, jrd_rel* ownerRelation, ExternalAccess::exa_act operation, jrd_rel* view); static void preModifyEraseTriggers(thread_db* tdbb, TrigVector** trigs, @@ -658,9 +659,12 @@ const StmtNode* BlockNode::execute(thread_db* tdbb, jrd_req* request, ExeState* return parentStmt; } + // Skip PSQL exception handlers when request restart is in progress + const bool skipHandlers = (transaction->tra_flags & TRA_ex_restart); + const StmtNode* temp = parentStmt; - if (handlers && handlers->statements.hasData()) + if (handlers && handlers->statements.hasData() && !skipHandlers) { // First of all rollback failed work if (!(transaction->tra_flags & TRA_system)) @@ -2267,6 +2271,7 @@ static RegisterNode regEraseNode(blr_erase); DmlNode* EraseNode::parse(thread_db* /*tdbb*/, MemoryPool& pool, CompilerScratch* csb, const UCHAR /*blrOp*/) { + const ULONG blrOffset = csb->csb_blr_reader.getOffset(); const USHORT n = csb->csb_blr_reader.getByte(); if (n >= csb->csb_rpt.getCount() || !(csb->csb_rpt[n].csb_flags & csb_used)) @@ -2275,6 +2280,10 @@ DmlNode* EraseNode::parse(thread_db* /*tdbb*/, MemoryPool& pool, CompilerScratch EraseNode* node = FB_NEW_POOL(pool) EraseNode(pool); node->stream = csb->csb_rpt[n].csb_stream; + ULONG marks; + if (csb->csb_dbg_info && csb->csb_dbg_info->blrToMarks.get(blrOffset, marks)) + node->marks |= marks; + return node; } @@ -2289,6 +2298,7 @@ StmtNode* EraseNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) if (dsqlCursorName.hasData() && dsqlScratch->isPsql()) { node->dsqlContext = dsqlPassCursorContext(dsqlScratch, dsqlCursorName, relation); + node->marks |= StmtNode::MARK_POSITIONED; // Process old context values. dsqlScratch->context->push(node->dsqlContext); @@ -2312,7 +2322,10 @@ StmtNode* EraseNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) RseNode* rse; if (dsqlCursorName.hasData()) + { rse = dsqlPassCursorReference(dsqlScratch, dsqlCursorName, relation); + node->marks |= StmtNode::MARK_POSITIONED; + } else { rse = FB_NEW_POOL(dsqlScratch->getPool()) RseNode(dsqlScratch->getPool()); @@ -2366,6 +2379,7 @@ string EraseNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, statement); NODE_PRINT(printer, subStatement); NODE_PRINT(printer, stream); + NODE_PRINT(printer, marks); return "EraseNode"; } @@ -2376,42 +2390,24 @@ void EraseNode::genBlr(DsqlCompilerScratch* dsqlScratch) const dsql_ctx* context; if (dsqlContext) - { context = dsqlContext; - - if (statement) - { - dsqlScratch->appendUChar(blr_begin); - statement->genBlr(dsqlScratch); - dsqlScratch->appendUChar(blr_erase); - GEN_stuff_context(dsqlScratch, context); - dsqlScratch->appendUChar(blr_end); - } - else - { - dsqlScratch->appendUChar(blr_erase); - GEN_stuff_context(dsqlScratch, context); - } - } else - { context = dsqlRelation->dsqlContext; - if (statement) - { - dsqlScratch->appendUChar(blr_begin); - statement->genBlr(dsqlScratch); - dsqlScratch->appendUChar(blr_erase); - GEN_stuff_context(dsqlScratch, context); - dsqlScratch->appendUChar(blr_end); - } - else - { - dsqlScratch->appendUChar(blr_erase); - GEN_stuff_context(dsqlScratch, context); - } + if (statement) + { + dsqlScratch->appendUChar(blr_begin); + statement->genBlr(dsqlScratch); } + dsqlScratch->appendUChar(blr_erase); + if (marks) + dsqlScratch->putDebugMarkers(marks); + GEN_stuff_context(dsqlScratch, context); + + if (statement) + dsqlScratch->appendUChar(blr_end); + if (message) dsqlScratch->appendUChar(blr_end); } @@ -2531,6 +2527,9 @@ EraseNode* EraseNode::pass2(thread_db* tdbb, CompilerScratch* csb) doPass2(tdbb, csb, statement.getAddress(), this); doPass2(tdbb, csb, subStatement.getAddress(), this); + if (!(marks & StmtNode::MARK_POSITIONED)) + forNode = pass2FindForNode(parentStmt, stream); + impureOffset = CMP_impure(csb, sizeof(SLONG)); csb->csb_rpt[stream].csb_flags |= csb_update; @@ -2543,6 +2542,7 @@ const StmtNode* EraseNode::execute(thread_db* tdbb, jrd_req* request, ExeState* if (request->req_operation == jrd_req::req_unwind) retNode = parentStmt; + else if (request->req_operation == jrd_req::req_return && subStatement) { if (!exeState->topNode) @@ -2620,6 +2620,12 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, jrd_req* request, WhichTrigger request->req_operation = jrd_req::req_return; RLCK_reserve_relation(tdbb, transaction, relation, true); + if (forNode && forNode->isWriteLockMode(request)) + { + VIO_writelock(tdbb, rpb, transaction); + return parentStmt; + } + // If the stream was sorted, the various fields in the rpb are probably junk. // Just to make sure that everything is cool, refetch and release the record. @@ -2649,14 +2655,23 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, jrd_req* request, WhichTrigger // Deleting new version blindly is generally unsafe, but is ok in this situation // because all changes made by this request will certainly be undone and request // will be restarted. - while (!VIO_erase(tdbb, rpb, transaction)) - { - // VIO_refetch_record returns false if record has been deleted by someone else. - // Gently ignore this situation and proceed further. - if (!VIO_refetch_record(tdbb, rpb, transaction, true, true)) - return parentStmt; - rpb->rpb_runtime_flags &= ~RPB_refetch; + if (forNode) + rpb->rpb_runtime_flags |= RPB_restart_ready; + else + rpb->rpb_runtime_flags &= ~RPB_restart_ready; + + if (!VIO_erase(tdbb, rpb, transaction)) + { + fb_assert(forNode != nullptr); + + forNode->setWriteLockMode(request); + + // VIO_writelock returns false if record has been deleted by someone else. + // Gently ignore this situation and proceed further. + rpb->rpb_runtime_flags |= RPB_refetch; + VIO_writelock(tdbb, rpb, transaction); + return parentStmt; } REPL_erase(tdbb, rpb, transaction); } @@ -3966,15 +3981,12 @@ const StmtNode* InAutonomousTransactionNode::execute(thread_db* tdbb, jrd_req* r jrd_tra* const org_transaction = request->req_transaction; fb_assert(tdbb->getTransaction() == org_transaction); - // Use SNAPSHOT isolation mode in autonomous transactions by default - ULONG transaction_flags = 0; - // Simulate legacy behavior if Read Consistency is not used - if (!dbb->dbb_config->getReadConsistency() && - !(org_transaction->tra_flags & TRA_read_consistency)) - { - transaction_flags = org_transaction->tra_flags; - } + ULONG transaction_flags = org_transaction->tra_flags; + + // Replace Read Consistency by Concurrecy isolation mode + if (transaction_flags & TRA_read_consistency) + transaction_flags &= ~(TRA_read_committed | TRA_read_consistency); jrd_tra* const transaction = TRA_start(tdbb, transaction_flags, org_transaction->tra_lock_timeout, @@ -4315,8 +4327,6 @@ void ExecBlockNode::genBlr(DsqlCompilerScratch* dsqlScratch) { thread_db* tdbb = JRD_get_thread_data(); - dsqlScratch->beginDebug(); - // Sub routine needs a different approach from EXECUTE BLOCK. // EXECUTE BLOCK needs "ports", which creates DSQL messages using the client charset. // Sub routine doesn't need ports and should generate BLR as declared in its metadata. @@ -4451,8 +4461,6 @@ void ExecBlockNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlScratch->appendUChar(blr_end); dsqlScratch->genReturn(true); dsqlScratch->appendUChar(blr_end); - - dsqlScratch->endDebug(); } // Revert parameters order for EXECUTE BLOCK statement @@ -4804,6 +4812,11 @@ DmlNode* ForNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* csb, { ForNode* node = FB_NEW_POOL(pool) ForNode(pool); + const ULONG blrOffset = csb->csb_blr_reader.getOffset(); + ULONG marks; + if (csb->csb_dbg_info && csb->csb_dbg_info->blrToMarks.get(blrOffset, marks)) + node->forUpdate = (marks & StmtNode::MARK_FOR_UPDATE) != 0; + if (csb->csb_blr_reader.peekByte() == (UCHAR) blr_stall) node->stall = PAR_parse_stmt(tdbb, csb); @@ -4898,6 +4911,8 @@ string ForNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, statement); NODE_PRINT(printer, cursor); NODE_PRINT(printer, parBlrBeginCnt); + NODE_PRINT(printer, forUpdate); + NODE_PRINT(printer, withLock); return "ForNode"; } @@ -4916,6 +4931,9 @@ void ForNode::genBlr(DsqlCompilerScratch* dsqlScratch) dsqlScratch->appendUChar(blr_for); + if (forUpdate) + dsqlScratch->putDebugMarkers(StmtNode::MARK_FOR_UPDATE); + if (!statement || dsqlForceSingular) dsqlScratch->appendUChar(blr_singular); @@ -4981,7 +4999,10 @@ StmtNode* ForNode::pass2(thread_db* tdbb, CompilerScratch* csb) // as implicit cursors are always positioned in a valid record, and the name is // only used to raise isc_cursor_not_positioned. - impureOffset = CMP_impure(csb, sizeof(SavNumber)); + if (rse->flags & RseNode::FLAG_WRITELOCK) + withLock = true; + + impureOffset = CMP_impure(csb, sizeof(Impure)); return this; } @@ -4989,17 +5010,21 @@ StmtNode* ForNode::pass2(thread_db* tdbb, CompilerScratch* csb) const StmtNode* ForNode::execute(thread_db* tdbb, jrd_req* request, ExeState* /*exeState*/) const { jrd_tra* transaction = request->req_transaction; + Impure* impure = request->getImpure(impureOffset); switch (request->req_operation) { case jrd_req::req_evaluate: - *request->getImpure(impureOffset) = 0; + // initialize impure values + impure->savepoint = 0; + impure->writeLockMode = false; + if (!(transaction->tra_flags & TRA_system) && transaction->tra_save_point && transaction->tra_save_point->hasChanges()) { const Savepoint* const savepoint = transaction->startSavepoint(); - *request->getImpure(impureOffset) = savepoint->getNumber(); + impure->savepoint = savepoint->getNumber(); } cursor->open(tdbb); request->req_records_affected.clear(); @@ -5011,11 +5036,42 @@ const StmtNode* ForNode::execute(thread_db* tdbb, jrd_req* request, ExeState* /* // fall into case jrd_req::req_sync: - if (cursor->fetchNext(tdbb)) { - request->req_operation = jrd_req::req_evaluate; - return statement; + const bool fetched = cursor->fetchNext(tdbb); + if (withLock) + { + const jrd_req* top_request = request->req_snapshot.m_owner; + if ((top_request) && (top_request->req_flags & req_update_conflict)) + impure->writeLockMode = true; + } + + if (fetched) + { + if (impure->writeLockMode && withLock) + { + // Skip statement execution and fetch (and try to lock) next record. + request->req_operation = jrd_req::req_sync; + return this; + } + + request->req_operation = jrd_req::req_evaluate; + return statement; + } } + + if (impure->writeLockMode) + { + const jrd_req* top_request = request->req_snapshot.m_owner; + fb_assert(top_request); + fb_assert(top_request->req_flags & req_update_conflict); + + transaction->tra_flags |= TRA_ex_restart; + + ERR_post(Arg::Gds(isc_deadlock) << + Arg::Gds(isc_update_conflict) << + Arg::Gds(isc_concurrent_transaction) << Arg::Num(top_request->req_conflict_txn)); + } + request->req_operation = jrd_req::req_return; // fall into @@ -5036,7 +5092,7 @@ const StmtNode* ForNode::execute(thread_db* tdbb, jrd_req* request, ExeState* /* default: { - const SavNumber savNumber = *request->getImpure(impureOffset); + const SavNumber savNumber = impure->savepoint; if (savNumber) { @@ -5060,6 +5116,21 @@ const StmtNode* ForNode::execute(thread_db* tdbb, jrd_req* request, ExeState* /* } +bool ForNode::isWriteLockMode(jrd_req* request) const +{ + const Impure* impure = request->getImpure(impureOffset); + return impure->writeLockMode; +} + + +void ForNode::setWriteLockMode(jrd_req* request) const +{ + Impure* impure = request->getImpure(impureOffset); + fb_assert(!impure->writeLockMode); + + impure->writeLockMode = true; +} + //-------------------- @@ -5451,6 +5522,8 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) if (returning) forNode->dsqlForceSingular = true; + forNode->forUpdate = true; + // Get the already processed relations. RseNode* processedRse = nodeAs(forNode->rse->dsqlStreams->items[0]); source = processedRse->dsqlStreams->items[0]; @@ -5488,6 +5561,7 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) // Build the MODIFY node. ModifyNode* modify = FB_NEW_POOL(pool) ModifyNode(pool); + modify->marks |= StmtNode::MARK_MERGE; thisIf->trueAction = modify; dsql_ctx* const oldContext = dsqlGetContext(target); @@ -5576,6 +5650,7 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) { // Build the DELETE node. EraseNode* erase = FB_NEW_POOL(pool) EraseNode(pool); + erase->marks |= StmtNode::MARK_MERGE; thisIf->trueAction = erase; dsql_ctx* context = dsqlGetContext(target); @@ -5640,6 +5715,7 @@ StmtNode* MergeNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) // Build the INSERT node. StoreNode* store = FB_NEW_POOL(pool) StoreNode(pool); + // TODO: store->marks |= StmtNode::MARK_MERGE; store->dsqlRelation = relation; store->dsqlFields = notMatched->fields; store->dsqlValues = notMatched->values; @@ -5904,6 +5980,7 @@ static RegisterNode regModifyNode2(blr_modify2); DmlNode* ModifyNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* csb, const UCHAR blrOp) { // Parse the original and new contexts. + const ULONG blrOffset = csb->csb_blr_reader.getOffset(); USHORT context = (unsigned int) csb->csb_blr_reader.getByte(); @@ -5940,6 +6017,10 @@ DmlNode* ModifyNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* c if (blrOp == blr_modify2) node->statement2 = PAR_parse_stmt(tdbb, csb); + ULONG marks; + if (csb->csb_dbg_info && csb->csb_dbg_info->blrToMarks.get(blrOffset, marks)) + node->marks |= marks; + return node; } @@ -5974,6 +6055,7 @@ StmtNode* ModifyNode::internalDsqlPass(DsqlCompilerScratch* dsqlScratch, bool up if (dsqlCursorName.hasData() && dsqlScratch->isPsql()) { node->dsqlContext = dsqlPassCursorContext(dsqlScratch, dsqlCursorName, relation); + node->marks |= StmtNode::MARK_POSITIONED; // Process old context values. dsqlScratch->context->push(node->dsqlContext); @@ -6054,6 +6136,7 @@ StmtNode* ModifyNode::internalDsqlPass(DsqlCompilerScratch* dsqlScratch, bool up { rse = dsqlPassCursorReference(dsqlScratch, dsqlCursorName, relation); old_context = rse->dsqlStreams->items[0]->dsqlContext; + node->marks |= StmtNode::MARK_POSITIONED; } else { @@ -6154,6 +6237,7 @@ string ModifyNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, mapView); NODE_PRINT(printer, orgStream); NODE_PRINT(printer, newStream); + NODE_PRINT(printer, marks); return "ModifyNode"; } @@ -6165,6 +6249,8 @@ void ModifyNode::genBlr(DsqlCompilerScratch* dsqlScratch) const dsql_msg* message = dsqlGenDmlHeader(dsqlScratch, rse); dsqlScratch->appendUChar(statement2 ? blr_modify2 : blr_modify); + if (marks) + dsqlScratch->putDebugMarkers(marks); const dsql_ctx* context; @@ -6358,6 +6444,9 @@ ModifyNode* ModifyNode::pass2(thread_db* tdbb, CompilerScratch* csb) SBM_SET(tdbb->getDefaultPool(), &csb->csb_rpt[orgStream].csb_fields, id); } + if (!(marks & StmtNode::MARK_POSITIONED)) + forNode = pass2FindForNode(parentStmt, orgStream); + impureOffset = CMP_impure(csb, sizeof(impure_state)); return this; @@ -6427,7 +6516,12 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, jrd_req* request, WhichTrigg { case jrd_req::req_evaluate: request->req_records_affected.bumpModified(false); - break; + + if (impure->sta_state == 0 && forNode && forNode->isWriteLockMode(request)) + request->req_operation = jrd_req::req_return; + // fall thru + else + break; case jrd_req::req_return: if (impure->sta_state == 1) @@ -6442,6 +6536,12 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, jrd_req* request, WhichTrigg if (impure->sta_state == 0) { + if (forNode && forNode->isWriteLockMode(request)) + { + VIO_writelock(tdbb, orgRpb, transaction); + return parentStmt; + } + // CVC: This call made here to clear the record in each NULL field and // varchar field whose tail may contain garbage. cleanupRpb(tdbb, newRpb); @@ -6461,19 +6561,26 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, jrd_req* request, WhichTrigg else if (!relation->rel_view_rse) { // VIO_modify returns false if there is an update conflict in Read Consistency - // transaction. Before returning false it disables statement-level snapshot + // transaction and our code is ready to work in write lock mode (see flag set below). + // Before returning false it disables statement-level snapshot // (via setting req_update_conflict flag) so re-fetch should see new data. - // Updating new version blindly is generally unsafe, but is ok in this situation - // because all changes made by this request will certainly be undone and request - // will be restarted. - while (!VIO_modify(tdbb, orgRpb, newRpb, transaction)) - { - // VIO_refetch_record returns false if record has been deleted by someone else. - // Gently ignore this situation and proceed further. - if (!VIO_refetch_record(tdbb, orgRpb, transaction, true, true)) - return parentStmt; - orgRpb->rpb_runtime_flags &= ~RPB_refetch; + if (forNode) + orgRpb->rpb_runtime_flags |= RPB_restart_ready; + else + orgRpb->rpb_runtime_flags &= ~RPB_restart_ready; + + if (!VIO_modify(tdbb, orgRpb, newRpb, transaction)) + { + fb_assert(forNode != nullptr); + + forNode->setWriteLockMode(request); + + // VIO_writelock returns false if record has been deleted by someone else. + // Gently ignore this situation and proceed further. + orgRpb->rpb_runtime_flags |= RPB_refetch; + VIO_writelock(tdbb, orgRpb, transaction); + return parentStmt; } IDX_modify(tdbb, orgRpb, newRpb, transaction); REPL_modify(tdbb, orgRpb, newRpb, transaction); @@ -8812,6 +8919,7 @@ static const dsql_msg* dsqlGenDmlHeader(DsqlCompilerScratch* dsqlScratch, RseNod if (dsqlRse) { dsqlScratch->appendUChar(blr_for); + dsqlScratch->putDebugMarkers(StmtNode::MARK_FOR_UPDATE); GEN_expr(dsqlScratch, dsqlRse); } @@ -9696,6 +9804,23 @@ static void pass1Validations(thread_db* tdbb, CompilerScratch* csb, Array(node)) + node = node->parentStmt; + + ForNode* forNode = nodeAs(node); + if (forNode && forNode->rse->containsStream(stream)) + { + //fb_assert(forNode->forUpdate == true); + if (forNode->forUpdate) + return forNode; + } + + return nullptr; +}; + // Inherit access to triggers to be fired. // // When we detect that a trigger could be fired by a request, diff --git a/src/dsql/StmtNodes.h b/src/dsql/StmtNodes.h index cdf6ad91f4..d530d6f316 100644 --- a/src/dsql/StmtNodes.h +++ b/src/dsql/StmtNodes.h @@ -35,6 +35,7 @@ namespace Jrd { class CompoundStmtNode; class ExecBlockNode; +class ForNode; class PlanNode; class RelationSourceNode; class SelectNode; @@ -564,7 +565,8 @@ public: dsqlContext(NULL), statement(NULL), subStatement(NULL), - stream(0) + stream(0), + marks(0) { } @@ -595,6 +597,8 @@ public: NestConst statement; NestConst subStatement; StreamType stream; + NestConst forNode; // parent implicit cursor, if present + unsigned marks; // see StmtNode::IUD_MARK_xxx }; @@ -913,7 +917,9 @@ public: rse(NULL), statement(NULL), cursor(NULL), - parBlrBeginCnt(0) + parBlrBeginCnt(0), + forUpdate(false), + withLock(false) { } @@ -927,7 +933,16 @@ public: virtual StmtNode* pass2(thread_db* tdbb, CompilerScratch* csb); virtual const StmtNode* execute(thread_db* tdbb, jrd_req* request, ExeState* exeState) const; + bool isWriteLockMode(jrd_req* request) const; + void setWriteLockMode(jrd_req* request) const; + public: + struct Impure + { + SavNumber savepoint; + bool writeLockMode; // true - driven statement (UPDATE\DELETE\SELECT WITH LOCK) works in "write lock" mode, false - normal mode + }; + NestConst dsqlSelect; NestConst dsqlInto; DeclareCursorNode* dsqlCursor; @@ -939,6 +954,8 @@ public: NestConst statement; NestConst cursor; int parBlrBeginCnt; + bool forUpdate; // part of UPDATE\DELETE\MERGE statement + bool withLock; // part of SELECT ... WITH LOCK statement }; @@ -1153,7 +1170,8 @@ public: validations(pool), mapView(NULL), orgStream(0), - newStream(0) + newStream(0), + marks(0) { } @@ -1190,6 +1208,8 @@ public: NestConst mapView; StreamType orgStream; StreamType newStream; + NestConst forNode; // parent implicit cursor, if present + unsigned marks; // see StmtNode::IUD_MARK_xxx }; diff --git a/src/dsql/dsql.cpp b/src/dsql/dsql.cpp index 61ad594c8a..cefe64ae1b 100644 --- a/src/dsql/dsql.cpp +++ b/src/dsql/dsql.cpp @@ -603,8 +603,12 @@ void DsqlDmlRequest::dsqlPass(thread_db* tdbb, DsqlCompilerScratch* scratch, boo else scratch->getStatement()->setBlrVersion(4); + scratch->beginDebug(); + GEN_request(scratch, node); + scratch->endDebug(); + // Create the messages buffers for (FB_SIZE_T i = 0; i < scratch->ports.getCount(); ++i) { @@ -856,13 +860,47 @@ void DsqlDmlRequest::execute(thread_db* tdbb, jrd_tra** traHandle, statement->getType() != DsqlCompiledStatement::TYPE_SAVEPOINT) { AutoSavePoint savePoint(tdbb, req_transaction); + req_request->req_flags &= ~req_update_conflict; int numTries = 0; while (true) { - doExecute(tdbb, traHandle, inMetadata, inMsg, outMetadata, outMsg, singleton); + AutoSetRestoreFlag restartReady(&req_request->req_flags, req_restart_ready, true); + try + { + doExecute(tdbb, traHandle, inMetadata, inMsg, outMetadata, outMsg, singleton); + } + catch (const status_exception&) + { + if (!(req_transaction->tra_flags & TRA_ex_restart)) + { + req_request->req_flags &= ~req_update_conflict; + throw; + } + } + if (!(req_request->req_flags & req_update_conflict)) + { + fb_assert((req_transaction->tra_flags & TRA_ex_restart) == 0); + req_transaction->tra_flags &= ~TRA_ex_restart; + +#ifdef DEV_BUILD + if (numTries > 0) + { + string s; + s.printf("restarts = %d", numTries); + + ERRD_post_warning(Arg::Warning(isc_random) << Arg::Str(s)); + } +#endif break; + } + + fb_assert((req_transaction->tra_flags & TRA_ex_restart) != 0); + req_request->req_flags &= ~req_update_conflict; + req_transaction->tra_flags &= ~TRA_ex_restart; + fb_utils::init_status(tdbb->tdbb_status_vector); + if (numTries >= 10) { gds__log("Update conflict: unable to get a stable set of rows in the source tables"); @@ -2076,7 +2114,15 @@ static void sql_info(thread_db* tdbb, { const bool detailed = (item == isc_info_sql_explain_plan); string plan = OPT_get_plan(tdbb, request->req_request, detailed); - +#ifdef DEV_BUILD + if (!detailed) + { + NodePrinter printer; + request->req_request->getStatement()->topNode->print(printer); + plan += "\n--------\n"; + plan += printer.getText(); + } +#endif if (plan.hasData()) { // 1-byte item + 2-byte length + isc_info_end/isc_info_truncated == 4 diff --git a/src/include/firebird/impl/consts_pub.h b/src/include/firebird/impl/consts_pub.h index 6252c49775..b341dd49e5 100644 --- a/src/include/firebird/impl/consts_pub.h +++ b/src/include/firebird/impl/consts_pub.h @@ -732,6 +732,7 @@ #define fb_dbg_subproc 5 #define fb_dbg_subfunc 6 #define fb_dbg_map_curname 7 +#define fb_dbg_map_markers 8 // sub code for fb_dbg_map_argument #define fb_dbg_arg_input 0 diff --git a/src/jrd/DebugInterface.cpp b/src/jrd/DebugInterface.cpp index 14a6e50193..d0e528e8b1 100644 --- a/src/jrd/DebugInterface.cpp +++ b/src/jrd/DebugInterface.cpp @@ -10,10 +10,10 @@ * See the License for the specific language governing rights * and limitations under the License. * - * The Original Code was created by Vlad Horsun + * The Original Code was created by Vlad Khorsun * for the Firebird Open Source RDBMS project. * - * Copyright (c) 2006 Vlad Horsun + * Copyright (c) 2006 Vlad Khorsun * and all contributors signed below. * * All Rights Reserved. @@ -225,6 +225,28 @@ void DBG_parse_debug_info(ULONG length, const UCHAR* data, DbgInfo& dbgInfo) break; } + case fb_dbg_map_markers: + { + if (data + 8 >= end) + { + bad_format = true; + break; + } + + ULONG marks = *data++; + marks |= *data++ << 8; + marks |= *data++ << 16; + marks |= *data++ << 24; + + ULONG offset = *data++; + offset |= *data++ << 8; + offset |= *data++ << 16; + offset |= *data++ << 24; + + dbgInfo.blrToMarks.put(offset, marks); + } + break; + case fb_dbg_end: if (data != end) bad_format = true; diff --git a/src/jrd/DebugInterface.h b/src/jrd/DebugInterface.h index f0295c4c56..b7f3836e5c 100644 --- a/src/jrd/DebugInterface.h +++ b/src/jrd/DebugInterface.h @@ -10,10 +10,10 @@ * See the License for the specific language governing rights * and limitations under the License. * - * The Original Code was created by Vlad Horsun + * The Original Code was created by Vlad Khorsun * for the Firebird Open Source RDBMS project. * - * Copyright (c) 2006 Vlad Horsun + * Copyright (c) 2006 Vlad Khorsun * and all contributors signed below. * * All Rights Reserved. @@ -33,7 +33,8 @@ // Also, it introduces some new tags. const UCHAR DBG_INFO_VERSION_1 = UCHAR(1); const UCHAR DBG_INFO_VERSION_2 = UCHAR(2); -const UCHAR CURRENT_DBG_INFO_VERSION = DBG_INFO_VERSION_2; +const UCHAR DBG_INFO_VERSION_3 = UCHAR(3); // blr offsets of "update" cursors and driven sub-statements +const UCHAR CURRENT_DBG_INFO_VERSION = DBG_INFO_VERSION_3; namespace Firebird { @@ -55,6 +56,7 @@ typedef Firebird::SortedArray< MapBlrToSrcItem> MapBlrToSrc; typedef GenericMap > > MapVarIndexToName; +typedef GenericMap > > MapBlrToMarks; struct ArgumentInfo { @@ -93,7 +95,8 @@ struct DbgInfo : public PermanentStorage argInfoToName(p), curIndexToName(p), subFuncs(p), - subProcs(p) + subProcs(p), + blrToMarks(p) { } @@ -126,6 +129,8 @@ struct DbgInfo : public PermanentStorage subProcs.clear(); } + + blrToMarks.clear(); } MapBlrToSrc blrToSrc; // mapping between blr offsets and source text position @@ -134,6 +139,7 @@ struct DbgInfo : public PermanentStorage MapVarIndexToName curIndexToName; // mapping between cursor index and name GenericMap > > subFuncs; // sub functions GenericMap > > subProcs; // sub procedures + MapBlrToMarks blrToMarks; // blr offsets of marked verbs }; } // namespace Firebird diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index de981c8c38..b245799eef 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -550,6 +550,8 @@ string RelationSourceNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, dsqlName); NODE_PRINT(printer, alias); NODE_PRINT(printer, context); + if (relation) + printer.print("rel_name", relation->rel_name); return "RelationSourceNode"; } diff --git a/src/jrd/Savepoint.cpp b/src/jrd/Savepoint.cpp index 1bb90eb997..41de5ab799 100644 --- a/src/jrd/Savepoint.cpp +++ b/src/jrd/Savepoint.cpp @@ -234,7 +234,7 @@ void VerbAction::mergeTo(thread_db* tdbb, jrd_tra* transaction, VerbAction* next release(transaction); } -void VerbAction::undo(thread_db* tdbb, jrd_tra* transaction, bool preserveLocks) +void VerbAction::undo(thread_db* tdbb, jrd_tra* transaction, VerbAction* preserveAction) { // Undo changes recorded for this verb action. // After that, clear the verb action and prepare it for later reuse. @@ -258,7 +258,7 @@ void VerbAction::undo(thread_db* tdbb, jrd_tra* transaction, bool preserveLocks) if (!DPM_get(tdbb, &rpb, LCK_read)) BUGCHECK(186); // msg 186 record disappeared - if ((have_undo || preserveLocks) && !(rpb.rpb_flags & rpb_deleted)) + if ((have_undo || preserveAction) && !(rpb.rpb_flags & rpb_deleted)) VIO_data(tdbb, &rpb, transaction->tra_pool); else CCH_RELEASE(tdbb, &rpb.getWindow(tdbb)); @@ -268,7 +268,7 @@ void VerbAction::undo(thread_db* tdbb, jrd_tra* transaction, bool preserveLocks) if (!have_undo) { - if (preserveLocks && rpb.rpb_b_page) + if (preserveAction && rpb.rpb_b_page) { // Fetch previous record version and update in place current version with it record_param temp = rpb; @@ -304,7 +304,10 @@ void VerbAction::undo(thread_db* tdbb, jrd_tra* transaction, bool preserveLocks) rpb.rpb_record = save_record; delete temp.rpb_record; - } else + + RBM_SET(transaction->tra_pool, &preserveAction->vct_records, rpb.rpb_number.getValue()); + } + else VIO_backout(tdbb, &rpb, transaction); } else @@ -437,8 +440,12 @@ Savepoint* Savepoint::rollback(thread_db* tdbb, Savepoint* prior, bool preserveL while (m_actions) { VerbAction* const action = m_actions; + VerbAction* preserveAction = nullptr; - action->undo(tdbb, m_transaction, preserveLocks); + if (preserveLocks && m_next) + preserveAction = m_next->getAction(action->vct_relation); + + action->undo(tdbb, m_transaction, preserveAction); m_actions = action->vct_next; action->vct_next = m_freeActions; diff --git a/src/jrd/Savepoint.h b/src/jrd/Savepoint.h index d6ff330258..e264ff2ba9 100644 --- a/src/jrd/Savepoint.h +++ b/src/jrd/Savepoint.h @@ -94,7 +94,7 @@ namespace Jrd UndoItemTree* vct_undo; // Data for undo records void mergeTo(thread_db* tdbb, jrd_tra* transaction, VerbAction* nextAction); - void undo(thread_db* tdbb, jrd_tra* transaction, bool preserveLocks); + void undo(thread_db* tdbb, jrd_tra* transaction, VerbAction* preserveAction); void garbageCollectIdxLite(thread_db* tdbb, jrd_tra* transaction, SINT64 recordNumber, VerbAction* nextAction, Record* goingRecord); diff --git a/src/jrd/exe.cpp b/src/jrd/exe.cpp index 8f899e24b5..3013b3bc12 100644 --- a/src/jrd/exe.cpp +++ b/src/jrd/exe.cpp @@ -875,7 +875,7 @@ void EXE_start(thread_db* tdbb, jrd_req* request, jrd_tra* transaction) TRA_post_resources(tdbb, transaction, statement->resources); TRA_attach_request(transaction, request); - request->req_flags &= req_in_use; + request->req_flags &= req_in_use | req_restart_ready; request->req_flags |= req_active; request->req_flags &= ~req_reserved; diff --git a/src/jrd/req.h b/src/jrd/req.h index 2403116118..3884c7261f 100644 --- a/src/jrd/req.h +++ b/src/jrd/req.h @@ -132,6 +132,7 @@ const USHORT RPB_refetch = 0x01; // re-fetch is required const USHORT RPB_undo_data = 0x02; // data got from undo log const USHORT RPB_undo_read = 0x04; // read was performed using the undo log const USHORT RPB_undo_deleted = 0x08; // read was performed using the undo log, primary version is deleted +const USHORT RPB_restart_ready = 0x10; // update conflict could be handled by statement restart const USHORT RPB_UNDO_FLAGS = (RPB_undo_data | RPB_undo_read | RPB_undo_deleted); @@ -399,6 +400,7 @@ const ULONG req_proc_fetch = 0x200L; // Fetch from procedure in progress const ULONG req_same_tx_upd = 0x400L; // record was updated by same transaction const ULONG req_reserved = 0x800L; // Request reserved for client const ULONG req_update_conflict = 0x1000L; // We need to restart request due to update conflict +const ULONG req_restart_ready = 0x2000L; // Request is ready to restat in case of update conflict // Index lock block diff --git a/src/jrd/tra.cpp b/src/jrd/tra.cpp index aebab96c64..52cd44d9bb 100644 --- a/src/jrd/tra.cpp +++ b/src/jrd/tra.cpp @@ -3866,6 +3866,9 @@ void jrd_tra::rollbackSavepoint(thread_db* tdbb, bool preserveLocks) { REPL_save_cleanup(tdbb, this, tra_save_point, true); + if (tra_flags & TRA_ex_restart) + preserveLocks = true; + Jrd::ContextPoolHolder context(tdbb, tra_pool); tra_save_point = tra_save_point->rollback(tdbb, NULL, preserveLocks); } diff --git a/src/jrd/tra.h b/src/jrd/tra.h index 03f424acdc..fe7af3778f 100644 --- a/src/jrd/tra.h +++ b/src/jrd/tra.h @@ -425,6 +425,7 @@ const ULONG TRA_no_auto_undo = 0x8000L; // don't start a savepoint in TRA_start const ULONG TRA_precommitted = 0x10000L; // transaction committed at startup const ULONG TRA_own_interface = 0x20000L; // tra_interface was created for internal needs const ULONG TRA_read_consistency = 0x40000L; // ensure read consistency in this transaction +const ULONG TRA_ex_restart = 0x80000L; // Exception was raised to restart request // flags derived from TPB, see also transaction_options() at tra.cpp const ULONG TRA_OPTIONS_MASK = (TRA_degree3 | TRA_readonly | TRA_ignore_limbo | TRA_read_committed | diff --git a/src/jrd/vio.cpp b/src/jrd/vio.cpp index dc562f5590..e1283736e2 100644 --- a/src/jrd/vio.cpp +++ b/src/jrd/vio.cpp @@ -1441,6 +1441,57 @@ void VIO_data(thread_db* tdbb, record_param* rpb, MemoryPool* pool) } +static bool check_prepare_result(int prepare_result, jrd_tra* transaction, jrd_req* request, record_param* rpb) +{ +/************************************** + * + * c h e c k _ p r e p a r e _ r e s u l t + * + ************************************** + * + * Functional description + * Called by VIO_modify and VIO_erase. Raise update conflict error if not in + * read consistency transaction or lock error happens or if request is already + * in update conflict mode. In latter case set TRA_ex_restart flag to correctly + * handle request restart. + * + **************************************/ + if (prepare_result == PREPARE_OK) + return true; + + jrd_req* top_request = request->req_snapshot.m_owner; + + const bool restart_ready = top_request && + (top_request->req_flags & req_restart_ready) && + (rpb->rpb_runtime_flags & RPB_restart_ready); + + // Second update conflict when request is already in update conflict mode + // means we have some (indirect) UPDATE\DELETE in WHERE clause of primary + // cursor. In this case all we can do is restart whole request immediately. + const bool secondary = top_request && + (top_request->req_flags & req_update_conflict) && + (prepare_result != PREPARE_LOCKERR); + + if (!(transaction->tra_flags & TRA_read_consistency) || prepare_result == PREPARE_LOCKERR || + secondary || !restart_ready) + { + if (secondary) + transaction->tra_flags |= TRA_ex_restart; + + ERR_post(Arg::Gds(isc_deadlock) << + Arg::Gds(isc_update_conflict) << + Arg::Gds(isc_concurrent_transaction) << Arg::Num(rpb->rpb_transaction_nr)); + } + + if (top_request) + { + top_request->req_flags |= req_update_conflict; + top_request->req_conflict_txn = rpb->rpb_transaction_nr; + } + return false; +} + + bool VIO_erase(thread_db* tdbb, record_param* rpb, jrd_tra* transaction) { /************************************** @@ -1911,20 +1962,8 @@ bool VIO_erase(thread_db* tdbb, record_param* rpb, jrd_tra* transaction) // Update stub didn't find one page -- do a long, hard update PageStack stack; int prepare_result = prepare_update(tdbb, transaction, tid_fetch, rpb, &temp, 0, stack, false); - if (prepare_result && - (!(transaction->tra_flags & TRA_read_consistency) || prepare_result == PREPARE_LOCKERR)) - { - ERR_post(Arg::Gds(isc_deadlock) << - Arg::Gds(isc_update_conflict) << - Arg::Gds(isc_concurrent_transaction) << Arg::Num(rpb->rpb_transaction_nr)); - } - if (prepare_result) - { - jrd_req* top_request = request->req_snapshot.m_owner; - top_request->req_flags |= req_update_conflict; - top_request->req_conflict_txn = rpb->rpb_transaction_nr; + if (!check_prepare_result(prepare_result, transaction, request, rpb)) return false; - } // Old record was restored and re-fetched for write. Now replace it. @@ -3216,20 +3255,8 @@ bool VIO_modify(thread_db* tdbb, record_param* org_rpb, record_param* new_rpb, j PageStack stack; int prepare_result = prepare_update(tdbb, transaction, org_rpb->rpb_transaction_nr, org_rpb, &temp, new_rpb, stack, false); - if (prepare_result && - (!(transaction->tra_flags & TRA_read_consistency) || prepare_result == PREPARE_LOCKERR)) - { - ERR_post(Arg::Gds(isc_deadlock) << - Arg::Gds(isc_update_conflict) << - Arg::Gds(isc_concurrent_transaction) << Arg::Num(org_rpb->rpb_transaction_nr)); - } - if (prepare_result) - { - jrd_req* top_request = tdbb->getRequest()->req_snapshot.m_owner; - top_request->req_flags |= req_update_conflict; - top_request->req_conflict_txn = org_rpb->rpb_transaction_nr; + if (!check_prepare_result(prepare_result, transaction, tdbb->getRequest(), org_rpb)) return false; - } IDX_modify_flag_uk_modified(tdbb, org_rpb, new_rpb, transaction); @@ -4085,8 +4112,18 @@ bool VIO_writelock(thread_db* tdbb, record_param* org_rpb, jrd_tra* transaction) if ((transaction->tra_flags & TRA_read_consistency)) { jrd_req* top_request = tdbb->getRequest()->req_snapshot.m_owner; - top_request->req_flags |= req_update_conflict; - top_request->req_conflict_txn = org_rpb->rpb_transaction_nr; + if (top_request && !(top_request->req_flags & req_update_conflict)) + { + if (!(top_request->req_flags & req_restart_ready)) + { + ERR_post(Arg::Gds(isc_deadlock) << + Arg::Gds(isc_update_conflict) << + Arg::Gds(isc_concurrent_transaction) << Arg::Num(org_rpb->rpb_transaction_nr)); + } + + top_request->req_flags |= req_update_conflict; + top_request->req_conflict_txn = org_rpb->rpb_transaction_nr; + } } org_rpb->rpb_runtime_flags |= RPB_refetch; return false;