Skip to content

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

Open
njnu-seafish wants to merge 51 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 51 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 [Fix-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

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.

@njnu-seafish njnu-seafish Sep 1, 2026

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.

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.

String sign = generateSign(alert);
alert.setSign(sign);
try {
int count = alertMapper.insertTaskResultAlertIfAbsent(alert);
if (count > 0) {
log.info("add task result alert to db , alert: {}", alert);
} else {
log.info("skip duplicate task result alert, sign: {}, workflowInstanceId: {}", sign,
alert.getWorkflowInstanceId());
}
return count;
} catch (DuplicateKeyException e) {
// Concurrent race: NOT EXISTS passed but another thread inserted first.
// The uk_alert_dedup unique constraint caught it — treat as a skip.
log.info("skip duplicate task result alert (concurrent race), sign: {}, workflowInstanceId: {}", sign,
alert.getWorkflowInstanceId());
return 0;
}

fully implements a dual-safeguard idempotent write mechanism combining application-layer deduplication with a database unique constraint as a fallback.

@njnu-seafish njnu-seafish Sep 9, 2026

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.

  1. Unique Index Added
    A unique composite index uk_alert_dedup has been created on the columns (sign, workflow_instance_id, alert_type) in the t_ds_alert table. This serves as the ultimate safeguard at the database level, ensuring that no duplicate alert records can ever be persisted, regardless of what happens at the application or SQL layer.

ALTER TABLE t_ds_alert ADD UNIQUE INDEX uk_alert_dedup (sign, workflow_instance_id, alert_type);

  1. Optimistic Check in SQL — Low Cost, No Exception Overhead
    The INSERT statement uses a WHERE NOT EXISTS clause to perform an optimistic duplicate check before inserting. In the vast majority of cases, if the alert already exists, the subquery detects it via the unique index and simply returns zero affected rows — no exception is thrown, no lock contention is incurred, and no error-handling overhead is introduced. This makes the common path both fast and lightweight.
    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)
    SELECT #{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}
    WHERE NOT EXISTS (
        SELECT 1 FROM t_ds_alert
        WHERE sign = #{alert.sign}
          AND workflow_instance_id = #{alert.workflowInstanceId}
          AND alert_type = #{alert.alertType.code}
    )
</insert>
  1. Pessimistic Fallback via DuplicateKeyException — Ensuring Consistency Under Extreme Concurrency
    In rare race conditions where two or more threads simultaneously pass the NOT EXISTS check and attempt to insert the same alert, the unique index will reject one of them, causing a DuplicateKeyException. The application catches this exception as a pessimistic fallback, treating it as a skip rather than a failure. This guarantees data consistency even under extreme concurrent load.

try {
int count = alertMapper.insertTaskResultAlertIfAbsent(alert);
if (count > 0) {
log.info("add task result alert to db , alert: {}", alert);
} else {
log.info("skip duplicate task result alert, sign: {}, workflowInstanceId: {}", sign,
alert.getWorkflowInstanceId());
}
return count;
} catch (DuplicateKeyException e) {
// Concurrent race: NOT EXISTS passed but another thread inserted first.
// The uk_alert_dedup unique constraint caught it — treat as a skip.
log.info("skip duplicate task result alert (concurrent race), sign: {}, workflowInstanceId: {}", sign,
alert.getWorkflowInstanceId());
return 0;
}

I clean up redundant comments and code.

@SbloodyS When you have a moment, could you please review the code again? Thanks so much!

@njnu-seafish

Copy link
Copy Markdown
Contributor Author

@SbloodyS When you have a moment, could you please review the code again? Thanks so much!

@SbloodyS SbloodyS modified the milestones: 3.4.3, 3.5.0 Sep 7, 2026
Comment on lines +61 to +75
<insert id="insertTaskResultAlertIfAbsent">
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)
SELECT #{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}
WHERE NOT EXISTS (
SELECT 1 FROM t_ds_alert
WHERE sign = #{alert.sign}
AND workflow_instance_id = #{alert.workflowInstanceId}
AND alert_type = #{alert.alertType.code}
)
</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.

Such logic should not be handled in the database, but should be handled by code.

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.

Such logic should not be handled in the database, but should be handled by code.

Thanks for the feedback. I've moved the dedup logic out of SQL and into application code.

The INSERT ... SELECT ... WHERE NOT EXISTS statement has been replaced with a plain INSERT ... VALUES. Idempotency is now handled entirely in code: AlertDao#addTaskResultAlert performs a direct insert and catches DuplicateKeyException (thrown by the uk_alert_dedup unique constraint) to treat the duplicate as a skip. The unique constraint remains as the DB-level safety net, but no dedup logic lives in the SQL itself.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] [Worker&SQL Task] In the SQL task type, when querying data, the configured alert did not take effect.

2 participants