-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[fix](dictionary) stabilize insert target during database drop #65476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| package org.apache.doris.dictionary; | ||
|
|
||
| import org.apache.doris.analysis.UserIdentity; | ||
| import org.apache.doris.catalog.Database; | ||
| import org.apache.doris.catalog.Env; | ||
| import org.apache.doris.common.AnalysisException; | ||
| import org.apache.doris.common.ClientPool; | ||
|
|
@@ -27,6 +28,7 @@ | |
| import org.apache.doris.common.Status; | ||
| import org.apache.doris.common.io.Text; | ||
| import org.apache.doris.common.io.Writable; | ||
| import org.apache.doris.common.util.DebugPointUtil; | ||
| import org.apache.doris.common.util.MasterDaemon; | ||
| import org.apache.doris.dictionary.Dictionary.DictionaryStatus; | ||
| import org.apache.doris.job.extensions.insert.InsertTask; | ||
|
|
@@ -267,6 +269,7 @@ public void dropDbDictionaries(String dbName) { | |
| // Log the drop operation | ||
| if (dbDictIds != null) { | ||
| for (Map.Entry<String, Long> entry : dbDictIds.entrySet()) { | ||
| idToDictionary.remove(entry.getValue()); | ||
| Env.getCurrentEnv().getEditLog().logDropDictionary(dbName, entry.getKey()); | ||
| } | ||
| // also drop all name mapping records. | ||
|
|
@@ -282,6 +285,19 @@ private boolean hasDictionaryWithoutLock(String dbName, String dictName) { | |
| return dbDictIds != null && dbDictIds.containsKey(dictName); | ||
| } | ||
|
|
||
| public boolean isCurrentDictionary(Dictionary dictionary) { | ||
| lockRead(); | ||
| try { | ||
| return isCurrentDictionaryWithoutLock(dictionary); | ||
| } finally { | ||
| unlockRead(); | ||
| } | ||
| } | ||
|
|
||
| private boolean isCurrentDictionaryWithoutLock(Dictionary dictionary) { | ||
| return idToDictionary.get(dictionary.getId()) == dictionary; | ||
| } | ||
|
|
||
| public Map<String, Dictionary> getDictionaries(String dbName) { | ||
| lockRead(); | ||
| try { | ||
|
|
@@ -412,6 +428,16 @@ public void dataLoad(ConnectContext ctx, Dictionary dictionary, boolean adaptive | |
| + dictionary.getStatus().name()); | ||
| } | ||
|
|
||
| Database database = Env.getCurrentInternalCatalog().getDbNullable(dictionary.getDbName()); | ||
| if (database == null || !isCurrentDictionary(dictionary)) { | ||
| dictionary.trySetStatus(oldStatus); | ||
| throw new AnalysisException("Dictionary " + dictionary.getName() + " has been dropped"); | ||
| } | ||
|
|
||
| while (DebugPointUtil.isEnable("DictionaryManager.dataLoad.blockBeforePlan")) { | ||
| Thread.sleep(10); | ||
| } | ||
|
|
||
| if (ctx == null) { // for run with scheduler, not by command. | ||
| // priv check is done in relative(caller) command. so use ADMIN here is ok. | ||
| ctx = InsertTask.makeConnectContext(UserIdentity.ADMIN, dictionary.getDbName()); | ||
|
|
@@ -433,7 +459,8 @@ public void dataLoad(ConnectContext ctx, Dictionary dictionary, boolean adaptive | |
| baseCommand.setJobId(DICTIONARY_JOB_ID); | ||
| } | ||
|
|
||
| InsertIntoDictionaryCommand command = new InsertIntoDictionaryCommand(baseCommand, dictionary, adaptiveLoad); | ||
| InsertIntoDictionaryCommand command = new InsertIntoDictionaryCommand( | ||
| baseCommand, database, dictionary, adaptiveLoad); | ||
|
|
||
| // run with sync by status. | ||
| try { | ||
|
|
@@ -464,8 +491,7 @@ public void dataLoad(ConnectContext ctx, Dictionary dictionary, boolean adaptive | |
| lockRead(); | ||
| boolean unlocked = false; | ||
| try { | ||
| if (!dictionaryIds.containsKey(dictionary.getDbName()) | ||
| || !dictionaryIds.get(dictionary.getDbName()).containsKey(dictionary.getName())) { | ||
| if (!isCurrentDictionaryWithoutLock(dictionary)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This identity check is still too early for the failure path below. After this block releases the dictionary-manager read lock, a concurrent
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is another cleanup mismatch in this same post-load section for partial refreshes. When |
||
| unlockRead(); | ||
| unlocked = true; | ||
|
|
||
|
|
@@ -495,16 +521,28 @@ public void dataLoad(ConnectContext ctx, Dictionary dictionary, boolean adaptive | |
| } | ||
| } | ||
|
|
||
| while (DebugPointUtil.isEnable("DictionaryManager.dataLoad.blockBeforeCommit")) { | ||
| Thread.sleep(10); | ||
| } | ||
|
|
||
| // commit and check the result. not modify metadata so dont need lock. | ||
| long stagedVersion = dictionary.getVersion(); | ||
| if (!commitNowVersion(ctx, dictionary)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This rollback still assumes commit failure means no BE made the staged version visible, but |
||
| if (!ctx.getStatementContext().isPartialLoadDictionary()) { | ||
| dictionary.decreaseVersion(); | ||
| Env.getCurrentEnv().getEditLog().logDictionaryDecVersion(dictionary); | ||
| // Keep the identity check and rollback journal ordered with DROP. | ||
| lockRead(); | ||
| try { | ||
| if (isCurrentDictionaryWithoutLock(dictionary) | ||
| && !ctx.getStatementContext().isPartialLoadDictionary()) { | ||
| dictionary.decreaseVersion(); | ||
| Env.getCurrentEnv().getEditLog().logDictionaryDecVersion(dictionary); | ||
| } | ||
| } finally { | ||
| unlockRead(); | ||
| } | ||
| dictionary.trySetStatus(oldStatus); | ||
| abortSpecificVersion(ctx, dictionary, dictionary.getVersion() + 1); | ||
| abortSpecificVersion(ctx, dictionary, stagedVersion); | ||
| throw new RuntimeException("Dictionary " + dictionary.getName() + " commit version " | ||
| + (dictionary.getVersion() + 1) + " failed"); | ||
| + stagedVersion + " failed"); | ||
| } | ||
|
|
||
| // commit succeed. update metadata. | ||
|
|
@@ -525,6 +563,10 @@ public void dataLoad(ConnectContext ctx, Dictionary dictionary, boolean adaptive | |
| } | ||
|
|
||
| private boolean commitNowVersion(ConnectContext ctx, Dictionary dictionary) { | ||
| if (DebugPointUtil.isEnable("DictionaryManager.commitNowVersion.forceFailure")) { | ||
| return false; | ||
| } | ||
|
|
||
| // use the same BEs when we get before start loading. | ||
| List<Backend> beList = ctx.getStatementContext().getUsedBackendsDistributing(); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,7 @@ | |
| import org.apache.logging.log4j.Logger; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.CopyOnWriteArrayList; | ||
|
|
||
|
|
@@ -107,8 +108,18 @@ public AbstractInsertExecutor(ConnectContext ctx, TableIf table, String labelNam | |
| */ | ||
| public AbstractInsertExecutor(ConnectContext ctx, TableIf table, String labelName, NereidsPlanner planner, | ||
| Optional<InsertCommandContext> insertCtx, boolean emptyInsert, long jobId, boolean needRegister) { | ||
| this(ctx, table.getDatabase(), table, labelName, planner, insertCtx, emptyInsert, jobId, needRegister); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This overload still lets normal insert retry attempts recover the database from the stale |
||
| } | ||
|
|
||
| /** | ||
| * Constructor for targets whose owning database cannot be recovered from the table during concurrent metadata | ||
| * changes. | ||
| */ | ||
| public AbstractInsertExecutor(ConnectContext ctx, DatabaseIf<?> database, TableIf table, String labelName, | ||
| NereidsPlanner planner, Optional<InsertCommandContext> insertCtx, boolean emptyInsert, long jobId, | ||
| boolean needRegister) { | ||
| this.ctx = ctx; | ||
| this.database = table.getDatabase(); | ||
| this.database = Objects.requireNonNull(database, "database should not be null"); | ||
| this.insertLoadJob = new InsertLoadJob(database.getId(), labelName, jobId); | ||
| if (needRegister) { | ||
| ctx.getEnv().getLoadManager().addLoadJob(insertLoadJob); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pre-commit failure paths still do not clean up staged dictionary data on BEs. A dictionary sink stages the refresh at EOS via
DictionaryFactory::refresh_dict(), but if one BE has staged data andcommand.run()later throws, or leaves an error inctx.getState(), the following catch/error branches only restore FE status and rethrow. They never callabortSpecificVersion(), and the later dropped-dictionary/commit-failure abort blocks are not reached. Because BE status collection anddelete_dict()only look at committed dictionaries, the_refreshing_dict_mapentry can remain invisible until another refresh overwrites it. Please abort the staged version in both pre-commit error paths when the selected BEs are known, using the current version for partial loads andversion + 1for full loads.