Request icon
OutputValues icon
SlackIncomingWebhook icon
If icon
Log icon
LoopUntil icon
Fail icon
Pause icon
Download icon
Submit icon
Sleep icon
Schedule icon

Flink Stateful Job Savepoint and Restart Drill

Savepoint a running Flink job, stop and relaunch it from that savepoint, then re-verify it resumed from the savepoint and is still consuming.

Categories
DataInfrastructure

A long running Flink streaming job holds state in memory and in its checkpoints. Restarting it for a config change, a version bump or a node drain means stopping it and starting it again. A plain stop and start throws that state away. The job then reprocesses from the start or from the latest offsets with empty operator state, which breaks exactly the aggregations and the deduplication the job exists for. The safe path is a savepoint: snapshot the state, stop from that snapshot, start the new run from it.

This blueprint runs that savepoint and restart as a drill against a real Flink cluster. It finds the running job through the REST API, takes a savepoint and in remediate mode stops the job with a savepoint then relaunches it from that savepoint with the io.kestra.plugin.flink.Submit task. It then reads the cluster again and fails the run unless a job is running from the savepoint and its input record count is still climbing. A savepoint only dry run proves the state can be captured without touching the live job.

This blueprint was created by zkasuran.

Safety

The restart is destructive: it stops a live streaming job. It runs only when auto_remediate is true and dry_run is false, then only after a human approves the approve_restart pause. The default inputs and the schedule leave the flow in savepoint only mode.

The savepoint and the stop with savepoint are driven through the Flink REST API with io.kestra.plugin.core.http.Request. The dedicated TriggerSavepoint and CancelJob tasks in the bundled plugin build their status poller with a retry interval equal to its own ceiling, which Kestra rejects before the first poll, so they cannot complete a savepoint on this version. The relaunch uses the Submit task, which works. The REST calls hit the same endpoints those tasks would.

How it works

  1. detect (io.kestra.plugin.core.http.Request) reads GET /jobs/overview from the Flink REST API.
  2. assess (io.kestra.plugin.core.output.OutputValues) keeps the RUNNING jobs, applies the job_name filter and picks the target. findings_alert posts to Slack naming the job the drill will touch. drill (io.kestra.plugin.core.flow.If) runs the rest only when a target exists.
  3. describe and baseline read the target job and sum read-records across its vertices as the before number.
  4. trigger_savepoint posts POST /jobs/{id}/savepoints with cancel-job false. wait_savepoint (io.kestra.plugin.core.flow.LoopUntil) polls the request until it is COMPLETED. read_savepoint reads the savepoint path. savepoint_guard fails the run if the savepoint did not complete or the job did not stay RUNNING, so a dry run that disturbed the job is caught.
  5. remediate_gate requires auto_remediate true and dry_run false. approve_restart (io.kestra.plugin.core.flow.Pause) holds for a human.
  6. On approval, stop_with_savepoint posts POST /jobs/{id}/stop, wait_stop polls it to COMPLETED and read_stop_savepoint reads the new savepoint path. choose_savepoint uses savepoint_path_override when set, otherwise that path.
  7. download_jar (io.kestra.plugin.core.http.Download) fetches the job JAR into Kestra storage. restore (io.kestra.plugin.flink.Submit) relaunches the job with restoreFromSavepoint set to the chosen path.
  8. verify_restart samples the relaunched job twice, reads GET /jobs/{id}/checkpoints for the restore record, then verify fails the run through io.kestra.plugin.core.execution.Fail unless the job is RUNNING, its latest.restored.external_path is the savepoint it was told to use and its read-records advanced by at least min_records_delta. A relaunch that returned no job id fails too.
  9. errors alerts Slack when the flow fails, for example when the REST API is unreachable.

What you get

  • A savepoint of the live job on demand, with the path it was written to, taken without stopping the job.
  • A full restart drill that stops the job with a savepoint and brings it back from that savepoint, so you know the restart keeps state before you need it during an incident.
  • A verification that reads the cluster after the relaunch and fails the run unless the job came back from the savepoint and is reading records again. A restart that silently started from scratch does not pass.
  • A Slack alert on the job the drill targets and on any failure.

Who it's for

  • Teams running stateful Flink streaming jobs that cannot lose state across a restart or an upgrade.
  • Platform engineers who want a rehearsed, approved savepoint and restart path instead of a manual sequence of REST calls under pressure.
  • Anyone preparing a Flink version bump or a node drain who wants proof the state survives first.

Why orchestrate this with Kestra

A safe restart is several ordered REST calls with waits in between: trigger a savepoint, poll it to completion, stop with a savepoint, poll again, relaunch from the exact path, then read the cluster back. Kestra adds the approval gate before anything stops, the record count comparison that decides whether the run passes or fails, the Slack alerts and an execution history that shows every drill and the savepoint path it used. The relaunch goes through the typed io.kestra.plugin.flink.Submit task.

Tested end to end

On Kestra 2.0.5 OSS against a real Flink 1.20.5 standalone cluster (one JobManager, one TaskManager) on the same Docker network, with a local HTTP sink standing in for Slack. The JobManager and the TaskManager shared one savepoint and checkpoint volume, with execution.checkpointing.interval set to 10s. The job under drill was the Flink StateMachineExample, an unbounded keyed streaming job that reads a generated event stream. Every number below was confirmed against the Flink REST API directly (job state, the savepoint path on the shared volume and the read-records metric), not only through Kestra's log.

Run Inputs Result
detect, no target defaults, no job running execution 7KODBYMFuON8426yK1NxBQ succeeded with state no target. Nothing was touched
dry run savepoint dry_run true against a running StateMachineExample execution 6RuNAVKNCuP1jpSSx7CWy9 took savepoint savepoint-2e2b34-d07eb4f31d58, its _metadata file was present on the shared volume and the job stayed RUNNING
restart auto_remediate true, dry_run false, approved execution 7bEi59j7wjGR4IayiGAn4c stopped the job with savepoint savepoint-2e2b34-9144bca657b7 then relaunched job a9b023fcddc920f55565a4ff65fc5baa. The relaunched job reported latest.restored.external_path equal to that savepoint with is_savepoint true, so it resumed from the savepoint. Its read-records went from 0 to 24413 across the two in run samples. A later REST sample read 39522014 then 39580463 over 14s, so it kept consuming
failure, bad savepoint savepoint_path_override set to a path that does not exist, approved execution 3QgQHKQJwsczE1NcMUH7Nz stopped the job with savepoint savepoint-a9b023-ba802c3b6e04, the relaunch returned HTTP 400 with no job id, verify failed the run and the errors block posted to Slack. No job was left running, which is the loss the drill is built to catch
REST API down rest_url at a host that does not resolve execution 6claRATZ8ZZF2Sto5h8DfZ failed at detect and the errors block posted to Slack

The savepoint paths and the record counts above were read from the Flink REST API and the shared volume directly, not only from Kestra's log. The relaunched job restored keyed state from the savepoint while its read-records counter started again at 0, which is correct: the counter is per run while the state is what the savepoint carries. The restore record is the proof it resumed.

Prerequisites

  • A Flink cluster whose REST API is reachable from Kestra at rest_url.
  • A savepoint directory every JobManager and TaskManager can read and write (one shared volume or object storage such as s3://). A savepoint written on one node and read on another fails when the path is local to a single container.
  • Checkpointing or at least a configured state backend on the cluster, so savepoints can be written.
  • For a restore, the job JAR at jar_url reachable from Kestra and its entry_class. The flow downloads the JAR into Kestra storage, because the Submit task reads the JAR from Kestra storage rather than an arbitrary URL.

Secrets

  • SLACK_WEBHOOK_URL: used by findings_alert and the errors block. In Kestra OSS, provide it as the base64 encoded environment variable SECRET_SLACK_WEBHOOK_URL.

Inputs

Input Default Purpose
rest_url http://flink-jobmanager:8081 Flink JobManager REST base URL
job_name empty Name filter, empty picks the first running job
jar_url empty Job JAR URL, fetched into Kestra storage for the relaunch
entry_class empty Fully qualified main class used on the relaunch
parallelism 2 Parallelism for the relaunched job
savepoint_dir file:///flink-savepoints Shared directory where savepoints are written
drain false Drain streaming input before the stop with savepoint
allow_non_restored_state false Skip savepoint state that no longer maps to the job graph
savepoint_path_override empty Relaunch from this exact path instead of the stop savepoint
min_records_delta 1 Records the relaunched job must gain between the two samples
poll_seconds 180 Timeout for a savepoint or a stop with savepoint
sample_seconds 12 Seconds between the two record count samples
auto_remediate false Must be true before a restart
dry_run true Must be false before a restart

Quick start

  1. Add the SLACK_WEBHOOK_URL secret.
  2. Set rest_url to your Flink JobManager REST URL and run with the defaults. The flow takes a savepoint of the running job and leaves it running. Read the savepoint path in the logs.
  3. Set job_name if more than one job runs and you want a specific one.
  4. To rehearse the restart, set jar_url and entry_class for the job, set auto_remediate to true and dry_run to false, run it and approve the paused execution. The flow stops the job with a savepoint, relaunches it from that savepoint and fails if it did not come back consuming.
  5. Enable the daily trigger once the inputs match your cluster. It stays in savepoint only mode.

How to extend

  • Loop over several jobs and drill each one in a single execution.
  • Store the savepoint path in a KV pair so a later run can relaunch from a known good savepoint with savepoint_path_override.
  • Point savepoint_dir at object storage so the savepoint survives the loss of any single node.
  • Add a step that bumps the JAR to a new version on relaunch, with allow_non_restored_state true, to turn the drill into an upgrade.
  • Compare read-records against the before number over a longer window to assert throughput, not just that the count moved.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.