Skip to content

[Fix-17854] [Worker&SQL Task] Fix SQL task query result alert not being sent - #18549

Open
njnu-seafish wants to merge 42 commits into
apache:devfrom
njnu-seafish:Fix-17854-2
Open

[Fix-17854] [Worker&SQL Task] Fix SQL task query result alert not being sent#18549
njnu-seafish wants to merge 42 commits into
apache:devfrom
njnu-seafish:Fix-17854-2

Conversation

@njnu-seafish

Copy link
Copy Markdown
Contributor

Was this PR generated or assisted by AI?

Yes, I design the architecture and write the core code myself, then use an LLM to review and optimize the logic.

Purpose of the pull request

close #17854

Brief change log

Purpose

Fix #17854: the SQL task "query result" alert feature silently stopped working after the
task-executor refactoring (DSIP-73). The alert flag and payload (needAlert /
taskAlertInfo) used to live on AbstractTask, but no component consumed them anymore,
so enabling "Send Alert" on a SQL task had no effect.

Root cause

The needAlert / taskAlertInfo fields were only defined and set in the task plugin
(AbstractTask), while the Master never read them. After the task-executor module
refactor, the success lifecycle event did not carry the alert information to the Master,
so the alert was never persisted/sent.

What changed

  • Task plugin side

    • Moved needAlert / taskAlertInfo from AbstractTask into TaskExecutionContext
      so they can be carried across the Worker -> Master RPC.
    • SqlTask: prepare the alert info (title, alertGroupId, AlertType.TASK_RESULT)
      and truncate the query result to displayRows (default if unset) to avoid oversized
      RPC payloads; empty result sets are also covered.
    • Renamed the SQL task parameter sendEmail to sendAlert (kept @JsonAlias("sendEmail")
      for backward-compatible deserialization) and removed the obsolete showType field.
  • Event / Master

    • TaskExecutorSuccessLifecycleEvent now carries needAlert and taskAlertInfo.
    • TaskExecutorEventListenerImpl consumes the success event: when needAlert is true
      and a valid alertGroupId is present, it delegates to WorkflowAlertManager.sendTaskResultAlert
      (with project / workflow / task context filled in); otherwise it logs a warning instead
      of silently dropping the alert.
  • Alert chain

    • Added AlertType.TASK_RESULT (8).
    • AlertSendRequest now carries AlertType instead of a plain int warnType;
      AlertSender.syncHandler and AlertOperatorImpl propagate it into AlertData.
  • Data migration & docs

    • Upgrade DML for MySQL / PostgreSQL migrates sendEmail -> sendAlert in
      t_ds_task_definition and t_ds_task_definition_log (null-safe guards added).
    • Documented the incompatible change in incompatible.md (en/zh).

Verification

  • Unit tests added/updated:
    • SqlParametersTest: JSON backward compatibility (sendEmail -> sendAlert) and
      new field name.
    • AlertSenderTest: syncHandler with the new AlertType argument.
  • Local build of the touched modules passes (mvn compile).

I previously submitted a PR proposing that the Worker role should directly send RPC requests to the Master to transmit SQL result set alerts. The proposal was rejected. (#17856)

Verify this pull request

This pull request is code cleanup without any test coverage.

(or)

This pull request is already covered by existing tests, such as (please describe tests).

(or)

This change added tests and can be verified as follows:

(or)

Pull Request Notice

Pull Request Notice

If your pull request contains incompatible change, you should also add it to docs/docs/en/guide/upgrade/incompatible.md

@njnu-seafish

Copy link
Copy Markdown
Contributor Author

@SbloodyS #17854 This issue was automatically closed by the bot due to inactivity. Could a maintainer please reopen it? I've just submitted a more reasonable solution to fix the SQL query task result alerting issue. Thanks so much!

@github-actions github-actions Bot added UI ui and front end related backend test document labels Aug 12, 2026
@SbloodyS SbloodyS changed the title [Bug-17854] [Worker&SQL Task] Fix SQL task query result alert not being sent Aug 12, 2026
@SbloodyS SbloodyS added the bug Something isn't working label Aug 12, 2026
@SbloodyS SbloodyS added this to the 3.5.0 milestone Aug 12, 2026
Comment thread docs/docs/en/guide/upgrade/incompatible.md Outdated
@njnu-seafish
njnu-seafish requested a review from SbloodyS August 13, 2026 09:16

@SbloodyS SbloodyS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserve the existing sendEmail field name

SqlParameters renames the persisted/API field from sendEmail to sendAlert, and the UI now only reads/writes sendAlert.

Although @JsonAlias("sendEmail") keeps deserialization compatible, serialization and UI payloads use the new name. This can cause existing clients, SDKs, integrations, or mixed-version components to silently lose the setting. It also introduces an unnecessary database migration and an incompatible public contract change.

Please keep sendEmail as the field name and only update the semantic description/UI label to indicate that alerts can use channels other than email. If the rename is still required, please provide an explicit compatibility strategy covering API clients, UI loading of legacy definitions, and rolling upgrades.

Keep AlertSendRequest wire-compatible

AlertSendRequest changes warnType: int to alertType: AlertType.

This changes both the field name and the serialized type of the Master–Alert RPC request. During a rolling upgrade, an old Alert Server will still expect warnType, while a new Alert Server may receive a request without alertType from an old Master. The result can be a default/incorrect alert type or a NullPointerException at alertType.getCode().

Please retain the existing warnType field for compatibility, or support both fields with explicit conversion and add a mixed-version serialization test.

@njnu-seafish

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Both issues have been addressed.

Thanks for the thorough review! Both issues have been addressed.

The previous field-name, ACK ordering, and task-identity issues have been addressed, but I found three remaining problems:

  1. TASK_RESULT is not compatible with an old Alert Server during rolling upgrades

File: dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/AlertType.java

A new Master now persists alert_type = 8. An old Alert Server does not have AlertType.TASK_RESULT, so MyBatis-Plus maps this unknown value to null. AlertSender#getAlertData() subsequently calls event.getAlertType().getCode(), causing a NullPointerException.

The alert remains in WAIT_EXECUTION and is not delivered while the old Alert Server is active. This is the database equivalent of the mixed-version RPC compatibility issue that was fixed earlier.

Please either reuse a value understood by old Alert Servers, add an explicitly safe upgrade strategy/order, or otherwise ensure that mixed-version deployments cannot write an enum value that an active consumer cannot deserialize. A mixed-version persistence test would be useful.

  1. Preserve the public AbstractTask alert API

File: dolphinscheduler-task-plugin/dolphinscheduler-task-api/src/main/java/org/apache/dolphinscheduler/plugin/task/api/AbstractTask.java

This PR removes the protected needAlert / taskAlertInfo fields and the public getNeedAlert, setNeedAlert, getTaskAlertInfo, and setTaskAlertInfo methods.

AbstractTask is part of the task-plugin extension API. Existing third-party task plugins compiled against these members can fail with NoSuchMethodError or NoSuchFieldError after upgrading, and source-compatible plugins will no longer compile.

Please retain these members as deprecated compatibility bridges that delegate to TaskExecutionContext. The SQL task can use the new context-based implementation without removing the old extension API immediately.

  1. The idempotent insert is still vulnerable to concurrent duplicates

File: dolphinscheduler-dao/src/main/resources/org/apache/dolphinscheduler/dao/mapper/AlertMapper.xml Around lines 59–79

INSERT ... SELECT ... HAVING count(*) = 0 is still a check-then-insert operation. There is no unique constraint on (sign, workflow_instance_id, alert_type), so two transactions handling the same success event can both observe a count of zero and insert duplicate alerts, especially on PostgreSQL.

The current tests only execute the inserts sequentially and therefore do not verify actual idempotency under concurrent delivery.

Please enforce the idempotency key at the database level and use an atomic conflict-handling insert, or provide another cross-database synchronization mechanism. A concurrent integration test should verify that exactly one row is inserted.

Thanks for the thorough review! Both issues have been addressed.

@SbloodyS SbloodyS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous ACK-ordering, task-identity, and database-constraint concerns have mostly been addressed, but the latest version still has the following issues.

1. The database-specific insert statements will not be selected

DaoConfiguration creates a plain VendorDatabaseIdProvider without defining any vendor mappings. Therefore, MyBatis uses the JDBC product names as database IDs: MySQL, PostgreSQL, and H2.

However, the new mapper statements use lowercase IDs:

  • databaseId="mysql"
  • databaseId="postgresql"
  • databaseId="h2"

These values are case-sensitive and do not match the IDs returned by the provider. As a result, MyBatis selects the generic plain INSERT statement instead. A duplicated lifecycle event will then violate uk_alert_dedup and throw an exception instead of returning zero, potentially preventing the Worker ACK and causing repeated retries.

Please either configure explicit mappings in VendorDatabaseIdProvider or use the actual database product names in the mapper statements.

The concurrent test currently hides this problem by catching and ignoring every exception. Future.isDone() only indicates completion; it does not report the exception. Please do not swallow these exceptions—call Future#get() and require every losing insert to return zero without throwing.

Also, consider avoiding MySQL INSERT IGNORE, because it can suppress unrelated data errors in addition to duplicate-key violations.

2. The protected AbstractTask fields are still removed

The deprecated getter and setter methods were restored, but the protected needAlert and taskAlertInfo fields are still removed.

Existing third-party task plugins may access these protected fields directly. Such plugins were compiled with field references to AbstractTask.needAlert and AbstractTask.taskAlertInfo, so they can still fail with NoSuchFieldError after upgrading.

Please retain the protected fields as deprecated compatibility members as well. Restoring only the methods does not preserve binary compatibility.

3. The null-alert-type fallback cannot protect an old Alert Server

The new fallback in AlertSender#getAlertData() cannot fix the documented rolling-upgrade scenario. An old Alert Server is running the old implementation, so it does not contain this null guard.

On a new Alert Server, TASK_RESULT is already known and will not be mapped to null. Therefore, this fallback only handles corrupted data or values introduced by an even newer version, and converting either case to WORKFLOW_INSTANCE_FAILURE silently sends an alert with the wrong semantic type.

Please rely on and enforce the documented component upgrade order, or preserve the unknown numeric value through deserialization. Do not silently relabel an unknown alert as a workflow failure.

@njnu-seafish

Copy link
Copy Markdown
Contributor Author

3. The null-alert-type fallback cannot protect an old Alert Server

The new fallback in AlertSender#getAlertData() cannot fix the documented rolling-upgrade scenario. An old Alert Server is running the old implementation, so it does not contain this null guard.

On a new Alert Server, TASK_RESULT is already known and will not be mapped to null. Therefore, this fallback only handles corrupted data or values introduced by an even newer version, and converting either case to WORKFLOW_INSTANCE_FAILURE silently sends an alert with the wrong semantic type.

Please rely on and enforce the documented component upgrade order, or preserve the unknown numeric value through deserialization. Do not silently relabel an unknown alert as a workflow failure.

thanks. doned

@njnu-seafish
njnu-seafish requested a review from SbloodyS August 28, 2026 08:28

@SbloodyS SbloodyS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TaskExecutionContext.getVarPool() is not populated with the predecessor-scoped VarPool

The latest change uses:

taskExecutionContext.getVarPool()

However, TaskExecutionContextFactory.createTaskExecutionContext() only writes the result of generateTaskInstanceVarPool() to:

taskInstance.setVarPool(VarPoolUtils.serializeVarPool(varPools));

TaskExecutionContextBuilder.buildTaskInstanceRelatedInfo() does not copy taskInstance.varPool into TaskExecutionContext, and TaskExecutionContext.varPool has no default value. Therefore, for a newly initialized sub-workflow logic task, taskExecutionContext.getVarPool() is normally null.

As a result, the current one-line change still drops runtime OUT parameters from upstream tasks. A manual test may appear to pass when the same parameter is also present in global parameters or the original workflow start parameters, but it does not verify propagation from the predecessor task's runtime output.

Please explicitly propagate the predecessor-scoped VarPool into the task execution context, or read the scoped VarPool from the current task instance. Also add automated regression tests covering:

  1. An OUT parameter produced only at runtime by an upstream task is passed to the sub-workflow.
  2. An OUT parameter from an unrelated sibling branch is not passed to the sub-workflow.
  3. Conflicting global/start/upstream parameters retain the intended precedence.
@njnu-seafish

njnu-seafish commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

TaskExecutionContext.getVarPool() is not populated with the predecessor-scoped VarPool

The latest change uses:

taskExecutionContext.getVarPool()

However, TaskExecutionContextFactory.createTaskExecutionContext() only writes the result of generateTaskInstanceVarPool() to:

taskInstance.setVarPool(VarPoolUtils.serializeVarPool(varPools));

TaskExecutionContextBuilder.buildTaskInstanceRelatedInfo() does not copy taskInstance.varPool into TaskExecutionContext, and TaskExecutionContext.varPool has no default value. Therefore, for a newly initialized sub-workflow logic task, taskExecutionContext.getVarPool() is normally null.

As a result, the current one-line change still drops runtime OUT parameters from upstream tasks. A manual test may appear to pass when the same parameter is also present in global parameters or the original workflow start parameters, but it does not verify propagation from the predecessor task's runtime output.

Please explicitly propagate the predecessor-scoped VarPool into the task execution context, or read the scoped VarPool from the current task instance. Also add automated regression tests covering:

  1. An OUT parameter produced only at runtime by an upstream task is passed to the sub-workflow.
  2. An OUT parameter from an unrelated sibling branch is not passed to the sub-workflow.
  3. Conflicting global/start/upstream parameters retain the intended precedence.

Thanks @SbloodyS. I'd like to clarify the scope here, because I believe this concern is out of scope for this PR.

.varPool(taskExecutionContext.getVarPool()) is pre-existing code on dev. In the diff of this PR it appears as an unchanged context line; this PR only adds the two adjacent lines .needAlert(...) and .taskAlertInfo(...):

.varPool(taskExecutionContext.getVarPool()) // unchanged (pre-existing on dev)

  • .needAlert(taskExecutionContext.isNeedAlert()) // added by this PR
  • .taskAlertInfo(taskExecutionContext.getTaskAlertInfo()) // added by this PR

This PR does not modify TaskExecutionContextFactory or TaskExecutionContextBuilder.

The alert path is decoupled from varPool. The SQL task-result alert travels through needAlert / taskAlertInfo, an independent channel that does not read taskExecutionContext.getVarPool(). SqlTask#prepareTaskResultAlert() only sets the alert info; it never reads varPool. So this PR neither affects nor depends on varPool propagation.

The varPool-propagation issue on sub-workflow logical tasks looks like a real pre-existing bug on dev, and I agree it's worth fixing — but it touches the shared TaskExecutionContext build path that all task types go through, so it belongs in a separate issue/PR with its own regression tests, rather than being mixed into an alert-focused PR (close #17854).

Could you double-check whether this comment was intended for another PR that actually touches the varPool/sub-workflow path? Could you double-check whether this comment was intended for another PR that actually touches the varPool/sub-workflow path? Happy to file a separate issue to track the varPool propagation fix if you confirm.

Comment on lines +55 to +88
<insert id="insertTaskResultAlertIfAbsent" databaseId="mysql">
INSERT INTO t_ds_alert(sign, title, content, alert_status, warning_type, log, alertgroup_id,
create_time, update_time, project_code, workflow_definition_code,
workflow_instance_id, alert_type)
VALUES (#{alert.sign}, #{alert.title}, #{alert.content}, #{alert.alertStatus.code},
#{alert.warningType.code}, #{alert.log}, #{alert.alertGroupId}, #{alert.createTime},
#{alert.updateTime}, #{alert.projectCode}, #{alert.workflowDefinitionCode},
#{alert.workflowInstanceId}, #{alert.alertType.code})
ON DUPLICATE KEY UPDATE id = id
</insert>

<!-- H2 -->
<insert id="insertTaskResultAlertIfAbsent" databaseId="h2">
MERGE INTO t_ds_alert(sign, title, content, alert_status, warning_type, log, alertgroup_id,
create_time, update_time, project_code, workflow_definition_code,
workflow_instance_id, alert_type)
KEY(sign, workflow_instance_id, alert_type)
VALUES (#{alert.sign}, #{alert.title}, #{alert.content}, #{alert.alertStatus.code},
#{alert.warningType.code}, #{alert.log}, #{alert.alertGroupId}, #{alert.createTime},
#{alert.updateTime}, #{alert.projectCode}, #{alert.workflowDefinitionCode},
#{alert.workflowInstanceId}, #{alert.alertType.code})
</insert>

<!-- PostgreSQL -->
<insert id="insertTaskResultAlertIfAbsent" databaseId="postgresql">
INSERT INTO t_ds_alert(sign, title, content, alert_status, warning_type, log, alertgroup_id,
create_time, update_time, project_code, workflow_definition_code,
workflow_instance_id, alert_type)
VALUES (#{alert.sign}, #{alert.title}, #{alert.content}, #{alert.alertStatus.code},
#{alert.warningType.code}, #{alert.log}, #{alert.alertGroupId}, #{alert.createTime},
#{alert.updateTime}, #{alert.projectCode}, #{alert.workflowDefinitionCode},
#{alert.workflowInstanceId}, #{alert.alertType.code})
ON CONFLICT (sign, workflow_instance_id, alert_type) DO NOTHING
</insert>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use this writing method, which will lead to poor database performance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use this writing method, which will lead to poor database performance.

Replaced the dialect-specific upsert (ON DUPLICATE KEY UPDATE / MERGE INTO / ON CONFLICT DO NOTHING) with a single dialect-neutral INSERT ... SELECT ... WHERE NOT EXISTS statement.

This approach follows the same pattern already used by insertAlertWhenServerCrash in the same mapper file, and avoids the following performance concerns with upsert syntax:

MySQL ON DUPLICATE KEY UPDATE: triggers a unique-index probe and may acquire gap/next-key locks even when the "update" is a no-op (id = id), adding lock contention under concurrent access.
H2 MERGE INTO: performs a separate SELECT then conditional INSERT, adding overhead on every call.
PostgreSQL ON CONFLICT: similarly incurs an extra index probe per insert.
The new SQL uses a single INSERT ... SELECT ... WHERE NOT EXISTS query. The NOT EXISTS subquery short-circuits on the first matching row via the uk_alert_dedup index, avoiding full-row scanning or locking overhead. This is one SQL statement, no dialect branches, and no upsert-specific locking.

The uk_alert_dedup unique constraint is retained as a concurrent-safety net. If a race condition causes two threads to pass NOT EXISTS simultaneously, the constraint catches the duplicate insert.
The DAO layer catches DuplicateKeyException and returns 0, ensuring idempotency without propagating exceptions to the caller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend bug Something isn't working document test

2 participants