danielnadean opened a new issue, #11777:
URL: https://github.com/apache/seatunnel/issues/11777

   ### Search before asking
   
   - [x] I searched open and closed SeaTunnel issues and STIPs for Zeta member 
removal, worker failover, retry counting, dirty jobs, recovery attempts, and 
failure history.
   - [x] STIP-15 covers dirty **data** collection, while this proposal defines 
an operational health label for a Zeta job. STIP-33 covers task failure 
history, but does not define job-level member-loss recovery counting or 
dirty-job semantics.
   
   ## Summary
   
   This STIP proposes a job-scoped **recovery-degraded** label, also referred 
to as a **dirty job**, for SeaTunnel Zeta.
   
   A job becomes dirty when the number of job-level deployment attempts 
associated with unexpected or unclassified Zeta member loss reaches a 
configured threshold. The label is additive metadata: it does not replace 
`JobStatus`, change retry limits, or stop the job.
   
   The proposal provides:
   
   - a precise distinction between member loss, recovery episodes, and 
deployment attempts;
   - job-level counting that does not multiply one recovery by the number of 
pipelines or task groups;
   - a stable attempt identity that survives active-master changes;
   - HA-backed running state and finished-job history;
   - explicit `CLEAN`, `DIRTY`, `UNKNOWN`, and `DISABLED` evaluation results;
   - additive REST fields for running and finished jobs;
   - an optional future path for trusted planned-leave exclusion.
   
   The term `dirty job` in this STIP is unrelated to dirty-data collection. It 
describes a job whose runtime continuity has degraded because member-loss 
recovery has happened too many times.
   
   ## Motivation
   
   In a Zeta cluster with two or more members, a worker process crash, machine 
failure, network isolation, Kubernetes Pod restart, or rolling replacement can 
remove a Hazelcast member. The current recovery path is approximately:
   
   ```text
   Hazelcast memberRemoved
     -> SeaTunnelServer.memberRemoved(...)
     -> CoordinatorService.memberRemoved(...)
     -> failedTaskOnMemberRemoved(...)
     -> task groups on the lost address become FAILED
     -> SubPlan checks the restore limit
     -> prepareRestorePipeline()
     -> resources are requested and the pipeline is deployed again
   ```
   
   The engine already restores pipelines and keeps an in-memory 
`pipelineRestoreNum`, but it does not provide a durable job-level answer to 
these questions:
   
   1. How many job-level deployment attempts were caused by member loss?
   2. Did one member-loss event affect multiple pipelines without being 
over-counted?
   3. Did one failed deployment lead to another deployment attempt without 
being under-counted?
   4. Did the tracking remain complete across an active-master change?
   5. Can operators query the result after the job finishes?
   
   This leaves repeatedly recovered streaming jobs looking healthy as long as 
their primary `JobStatus` returns to `RUNNING`.
   
   ## Current behavior and source boundaries
   
   The current source establishes several important constraints:
   
   - `SeaTunnelServer.memberRemoved(...)` forwards the Hazelcast membership 
event to the active coordinator.
   - `CoordinatorService.failedTaskOnMemberRemoved(...)` finds task groups 
deployed on the lost address and marks eligible states as `FAILED`.
   - `SubPlan.prepareRestorePipeline()` increments `pipelineRestoreNum` before 
resource release, resource request, and redeployment.
   - `pipelineRestoreNum` is pipeline-scoped and in-memory. It is not a 
job-level member-loss counter and is not exposed through finished-job history.
   - `MembershipServiceEvent` provides the lost member address and UUID, but 
not a Kubernetes termination reason, process exit code, or trusted 
planned-maintenance flag.
   - The current Kubernetes resource manager does not classify member-removal 
causes.
   
   Therefore, `memberRemoved` proves that a Hazelcast member left the cluster. 
It does **not** prove that a machine failed.
   
   ## Goals
   
   1. Mark a job dirty after `X` job-level deployment attempts associated with 
member loss.
   2. Count deployment attempts, not task-group failures, pipeline count, or 
raw member-removal count.
   3. Preserve member UUID and address as diagnostic context without using 
address as member identity.
   4. Avoid mixing ordinary connector, data, or user-code failures into this 
counter.
   5. Preserve the tracking state across active-master changes and 
partition-owner changes.
   6. Expose the same evaluation summary for running and finished jobs.
   7. Keep the existing job state machine, retry behavior, checkpoint format, 
and REST fields backward compatible.
   8. Fail open for job recovery but fail closed for observability: tracking 
failure must not block failover, and it must not be reported as `CLEAN`.
   
   ## Non-goals
   
   The first implementation will not:
   
   - add a new `JobStatus` such as `DIRTY_RUNNING`;
   - stop, cancel, or suspend a dirty job automatically;
   - replace Hazelcast membership failure detection with a new heartbeat 
detector;
   - diagnose whether a member left because of OOM, machine failure, network 
partition, or Kubernetes policy;
   - count ordinary business failures that have no member-loss cause;
   - add Prometheus metrics or Web UI filtering in the first backend PR;
   - implement trusted planned-leave exclusion in the first backend PR;
   - change `job.retry.times` semantics.
   
   ## Terminology
   
   ### Member-loss incident
   
   A `MemberLossIncident` records that one Hazelcast member UUID left and 
affected one job. Its stable identity is:
   
   ```text
   jobId + lostMemberUuid
   ```
   
   The incident is diagnostic and idempotent. It is not the dirty counter.
   
   ### Recovery episode
   
   A `RecoveryEpisode` is one continuous member-loss recovery process for a 
job. Multiple member-loss incidents and multiple affected pipelines may belong 
to the same episode.
   
   The episode begins when the coordinator registers the first affected 
member-loss incident. It completes when all affected pipelines have deployed 
successfully and recovered, or aborts when the job can no longer restore.
   
   ### Deployment attempt
   
   A `RecoveryDeploymentAttempt` is one concrete redeployment attempt for one 
affected pipeline. Restore preparation allocates a stable identity:
   
   ```text
   attemptId = episodeId + pipelineId + prepareSequence
   ```
   
   `prepareSequence` is incremented atomically in job-scoped dirty state. The 
attempt is counted only when the prepared pipeline reaches the recovery 
`DEPLOYING` stage after resource allocation.
   
   `prepareRestorePipeline()` alone does not count because no task group has 
been redeployed at that point.
   
   ### Job-level attempt generation
   
   Within one recovery episode, every affected pipeline maintains its own 
confirmed prepare sequence. The episode's job-level generation is the maximum 
confirmed sequence:
   
   - multiple pipelines entering their first deployment attempt produce 
generation `1`, not `N`;
   - if any pipeline requires a second deployment attempt, generation becomes 
`2`;
   - confirming the same stable attempt ID again is a no-op.
   
   The job's `recoveryAttemptCount` increases only when a confirmed generation 
advances. It continues accumulating across completed episodes for the same job 
ID.
   
   ## Dirty evaluation
   
   Proposed evaluation values:
   
   | Value | Meaning |
   | --- | --- |
   | `DISABLED` | The job did not enable dirty-job tracking. |
   | `CLEAN` | Tracking is complete and the threshold has not been reached. |
   | `DIRTY` | The threshold has been reached. This is irreversible for the 
current job ID. |
   | `UNKNOWN` | Tracking is incomplete or inconsistent, so the job must not be 
reported as clean. |
   
   The primary job status remains independent. For example, a job can be both 
`RUNNING` and `DIRTY`.
   
   ## Cause classification and Kubernetes semantics
   
   The first implementation uses these cause semantics:
   
   | Scenario | Classification | Count after deployment starts |
   | --- | --- | --- |
   | OOM, process crash, liveness restart | `UNCLASSIFIED` | Yes |
   | Manual Pod deletion without prior trusted intent | `UNCLASSIFIED` | Yes |
   | Rolling update without a trusted drain protocol | `UNCLASSIFIED` | Yes |
   | Ordinary task failure without `memberRemoved` | No incident | No |
   
   Unknown cause is intentionally counted because the engine cannot prove that 
the interruption was planned.
   
   A later phase may add `PLANNED_VERIFIED`, but only through an authenticated 
and authorized control plane that registers the exact member UUID, request ID, 
actor, reason, creation time, and short expiration time before termination. A 
Pod `preStop` hook or JVM shutdown hook must not be trusted to classify its own 
exit.
   
   If one recovery episode contains any `UNCLASSIFIED` incident, its deployment 
attempts are counted. The episode may be excluded only when every incident is 
`PLANNED_VERIFIED`.
   
   ## State model
   
   The first implementation introduces a dedicated HA-backed job state keyed by 
job ID:
   
   ```text
   engine_jobDirtyState: IMap<Long, DirtyJobState>
   ```
   
   `DirtyJobState` contains at least:
   
   - job ID and configured threshold;
   - `dirty`, `evaluationStatus`, and `trackingComplete`;
   - cumulative `recoveryAttemptCount`;
   - recovery episode and member-loss incident counts;
   - the active recovery episode;
   - a bounded recent-episode history;
   - first dirty time and last counted time;
   - the last processed member-event sequence.
   
   All updates for one job use a single-key `EntryProcessor` or an equivalent 
atomic operation. The processor must update synchronous backups. The map 
configuration must be installed consistently on every member before the map is 
first obtained.
   
   The state is bounded. Incident and episode history has a configured maximum, 
task-group details are sampled, and stale incidents that never reach deployment 
are expired or marked aborted.
   
   ## Tracking completeness and active-master recovery
   
   Job-scoped atomic updates do not form a distributed transaction with task 
and pipeline state. This proposal uses idempotent, at-least-once convergence 
instead of changing the existing job state machine into a distributed 
transaction.
   
   A global monotonically increasing member-event sequence is stored using an 
existing serializable type in running engine state. For every `memberRemoved` 
event:
   
   1. The active coordinator increments the global sequence before scanning 
jobs.
   2. Every dirty-enabled running job advances its own processed sequence after 
the event is handled.
   3. Affected jobs register or merge the incident in the same atomic 
dirty-state update.
   4. If dirty-state update fails, the job watermark remains behind the global 
sequence.
   5. A missing dirty state or a lagging watermark evaluates to `UNKNOWN`, 
never `CLEAN`.
   
   Dirty state is initialized before a dirty-enabled job becomes schedulable. 
This prevents a clean default from being inferred for a job that never created 
its state.
   
   After an active-master change, reconciliation runs before restored pipelines 
are scheduled:
   
   - a persisted `PREPARED` attempt is compared with the pipeline state;
   - `DEPLOYING` or `RUNNING` confirms the original stable attempt ID 
idempotently;
   - pre-deployment states retain the binding and wait for normal deployment;
   - terminal non-restorable state aborts the episode;
   - missing state, watermark lag, incompatible serialization, or an 
unexplained transition evaluates to `UNKNOWN`.
   
   Tracking-store failure must be logged and surfaced, but it must not block 
the existing task failure or failover path.
   
   ## Configuration
   
   The dirty threshold is separate from the retry limit:
   
   ```hocon
   env {
     job.retry.times = 5
     job.dirty-task.threshold = 2
   }
   ```
   
   Proposed options:
   
   | Option | Default | Meaning |
   | --- | ---: | --- |
   | `job.dirty-task.threshold` | `-1` | Disabled when less than or equal to 
zero. |
   | `engine.dirty-task.event-history-size` | `10` | Maximum recent episodes 
retained per job. |
   | `engine.dirty-task.pending-incident-ttl` | `10m` | Maximum lifetime of an 
incident that never reaches deployment. |
   
   `job.retry.times` remains the restore limit. `job.dirty-task.threshold` only 
controls the health label.
   
   ## REST and history contract
   
   Existing REST fields remain unchanged. Running and finished job detail adds 
an optional object:
   
   ```json
   {
     "dirtyTask": {
       "dirty": true,
       "evaluationStatus": "DIRTY",
       "trackingComplete": true,
       "threshold": 2,
       "recoveryAttemptCount": 2,
       "recoveryEpisodeCount": 1,
       "memberLossIncidentCount": 1,
       "activeEpisodeId": null,
       "lastLeaveClassification": "UNCLASSIFIED",
       "lastLostMemberUuid": "23cc7ea6-6374-4f2b-8a33-df75b5e58f52",
       "lastLostAddress": "10.0.0.12:5801",
       "lastCountedTime": 1786500000000
     }
   }
   ```
   
   Job lists should expose only `dirty`, `evaluationStatus`, and 
`recoveryAttemptCount`. Recent episode detail belongs only in job detail to 
avoid unbounded list responses.
   
   When a job finishes, `JobHistoryService` snapshots dirty state together with 
the finished-job lifecycle. Running dirty state is removed only after the final 
history write. Older finished-job records without this field deserialize as 
tracking disabled or absent, without changing existing fields.
   
   ## Compatibility and rollout
   
   This proposal is additive and disabled by default:
   
   - existing jobs require no configuration change;
   - `JobStatus`, retry limits, checkpoint/savepoint payloads, and existing 
REST fields are unchanged;
   - no connector SPI changes are required;
   - old finished-job records remain readable;
   - no new state object is created for disabled jobs.
   
   Because Hazelcast partition owners deserialize map values and entry 
processors, the first release requires a homogeneous engine cluster:
   
   1. Upgrade every member to binaries that contain the new serializers and 
tracker while keeping the option disabled.
   2. Confirm no old member remains.
   3. Enable the option only for newly submitted jobs.
   
   Before downgrading to a binary that does not understand the new types, stop 
creating enabled jobs and allow existing enabled jobs to finish and clean their 
running dirty state.
   
   ## Performance considerations
   
   Existing member-removal handling already scans running jobs. This proposal 
keeps the same big-O scan but adds one lightweight watermark update per 
dirty-enabled job and one bounded state update for each affected job.
   
   The implementation must measure member-removal processing latency with at 
least 100 and 1,000 concurrent jobs. The state retained per job is bounded by 
the configured history size and sampled task-group details.
   
   ## Implementation plan
   
   ### Phase 1: backend dirty-job tracking
   
   1. Add configuration and bounded dirty-state models.
   2. Add backup-aware atomic tracker operations and global member-event 
sequencing.
   3. Register member-loss incidents before existing task groups are failed.
   4. Allocate stable attempt IDs during restore preparation.
   5. Confirm attempts only when recovery reaches `DEPLOYING`.
   6. Complete or retain attempt context according to deployment outcome.
   7. Reconcile active state before scheduling after an active-master change.
   8. Persist finished history and expose additive REST fields.
   9. Add unit and integration coverage for idempotency, failure paths, HA 
recovery, and compatibility.
   
   ### Phase 2: trusted planned-leave exclusion
   
   This phase is independent and requires a reviewed authenticated operations 
control plane. It is not part of the first backend PR.
   
   ### Later optional work
   
   - Prometheus metrics and alert transitions;
   - Web UI badge, filtering, and episode detail;
   - faster stale-worker suspicion as a separate failure-detection proposal.
   
   ## Acceptance criteria
   
   - [ ] One member-loss episode affecting multiple pipelines counts one 
first-generation job attempt.
   - [ ] A second deployment attempt advances the job count exactly once.
   - [ ] Duplicate `memberRemoved` delivery and duplicate attempt confirmation 
are idempotent.
   - [ ] Ordinary task failures without an active member-loss recovery context 
are not counted.
   - [ ] Restore preparation without reaching `DEPLOYING` is not counted.
   - [ ] A completed episode cannot be consumed by a later ordinary business 
failure.
   - [ ] A Kubernetes Pod restart without trusted planned-leave intent is 
classified as `UNCLASSIFIED` and counted after deployment starts.
   - [ ] Active-master change before or during attempt confirmation neither 
loses nor duplicates the count.
   - [ ] Partition-owner change preserves state through a synchronous backup.
   - [ ] Missing state, watermark lag, or tracker failure returns `UNKNOWN`, 
not `CLEAN`.
   - [ ] Reaching the threshold does not alter the primary job status or retry 
behavior.
   - [ ] Running and finished jobs expose the same summary semantics.
   - [ ] The feature is disabled by default and old finished-job records remain 
readable.
   - [ ] Mixed-version clusters cannot enable the feature before every member 
has upgraded.
   - [ ] Member-removal processing remains bounded under the defined 
concurrency test matrix.
   
   ## Usage Scenario
   
   Operators running long-lived Zeta streaming jobs need to identify jobs that 
repeatedly recover after member loss even when those jobs return to `RUNNING`.
   
   The proposed contract answers:
   
   - Has this job exceeded the acceptable number of member-loss deployment 
attempts?
   - How many job-level attempts and recovery episodes occurred?
   - Which member UUID and address were involved?
   - Is the result complete, or should it be treated as unknown?
   - Can the same result be queried after the job finishes?
   
   ## Related issues
   
   - #11735: task exception and failure history contract
   - #11755: stale task completion across restore generations
   - #10804: state-store abstraction for Hazelcast IMap
   - #11472: finished metrics after worker failover
   - #10461: retry mechanism correctness
   
   ## Are you willing to submit a PR?
   
   - [x] Yes, I am willing to submit a Draft PR for Phase 1.
   
   ## Code of Conduct
   
   - [x] I agree to follow the Apache Software Foundation Code of Conduct.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to