Runs & Debugging
A job run tracks one execution of a job. Run Job, Save and Run Now, and each scheduled tick create a run. Retry Run resets the latest eligible run and executes it again with the same run ID and job version.
A continuous job keeps one run going instead, and that run restarts its code rather than ending.
A run started by the schedule shows Logical Time: the scheduled time it covers, which can be earlier than when it started. See Which time a run is for.
For the job definition, see Defining Jobs. For scheduling, see Scheduling.
Prerequisites
- A saved job that has been triggered at least once (manually or on schedule) so there's a run to inspect.
Run lifecycle
The status shown for a run tells you exactly where it is stuck.
| Status | Meaning |
|---|---|
| Pending | Run accepted but held until it is released |
| Ready | Eligible for execution |
| Starting | The job worker has claimed the run and is creating its Kubernetes resources |
| Creating Sail / Waiting for Sail | The worker deploys Sail (the query engine) and waits for it |
| Creating Runner | The worker creates the runner for your code |
| Running | The runner is installing the job's libraries or executing your code. Crash Looping beside it means the library install keeps failing and retries on its own, on any run, with Status Message quoting the error; or, for a continuous job, that the code keeps exiting and is restarted less often (see Continuous Jobs) |
| Succeeded / Failed / Cancelled / Timeout | Terminal; the run stays here unless it is retried. Failed, Cancelled and Timeout can be retried, except for a continuous job |
| Superseded | Terminal; a newer version of a continuous job took over the run |
| Unknown | The platform could not determine the run's state |
Inspect a run
Click into a run to see:
- Timeline and status message: where the run is and why it failed.
- Output: SQL output and its location in the network workspace bucket. Results with columns are stored as Parquet; statements that return no columns do not create a result file.
- Job and version: the definition used for the run.
- Parameters: the values the run used, defaults included. See Parameters.
- Timing: dispatch, runner, completion, and duration fields when available.
Logs
Each run has two logs, shown on the run's detail page under Logs:
- Runner: the process that runs your Python code and submits its work to Sail. Output from
print()andloggingappears here. Start here for setup problems, such as a file or library that could not be fetched. - Engine: the Sail driver that executes it. Engine-side failures such as out-of-memory kills and panics appear here.
Logs stream live while the run is in progress and stay readable after it finishes, until the run's resources are cleaned up. After cleanup they are gone; the run's status, timing, and error message remain. If a log is not available yet, its pod has not started; wait and try again.
Through the API, IssueJobRunLogStream returns a short-lived URL and token; a streaming GET on it follows the log until the container exits.
Recovery actions
Retry a run
Retry Run resets and re-dispatches the existing run. Its run ID, job version, and parameters stay the same. LakeSail clears the previous execution state and output, then starts a fresh workload attempt. This is separate from automatic restarts, which restart a failed runner within a run that has not ended yet.
The referenced saved query and current compute profile settings are resolved again when the retry starts. If either changed after the original attempt, the retry can run with those changes even though the job version stays the same.
Retry is available only when all of these are true:
- The job is not continuous.
- The run is the job's latest run.
- Its status is Failed, Timeout, or Cancelled.
- The job is active and the run still uses its current version.
- The cluster and billing state allow another execution.
To preserve the failed attempt as a separate run, use Run Job instead. A run that is still running cannot be restarted in place; that exists only for continuous jobs.
Hold and release a run
This flow is available through the API for orchestration. Create or retry a run with hold: true to leave it Pending. Call ReleaseJobRun when its dependencies are ready; release changes the run from Pending to Ready. It does not detach or reclaim compute.
Create a run safely from an orchestrator
A request to CreateJobRun can time out after the run was created. Sending it again blindly then creates two runs. To make a resend safe, send an Idempotency-Key header with a value you choose, such as the orchestrator's task instance ID and try number.
- The first request with a key creates the run and returns
201. - A later request from the same member with the same key for the same job returns that run with
200. It creates nothing, even if the job was paused since. - A request from another member with a used key is refused with
409, without details of the run. - A request that differs from the first one, for example in its parameters, is refused with
409. The message names the run the key created and the fields that differ.
A key belongs to its run for as long as the run exists, and the run's detail shows it. Keys are per job: the same key on another job creates a separate run.
Cancel a run
Cancel Run, on a run's page and in the runs list, stops a run that has not finished. It is CancelJobRun in the API. The run moves to Cancelled, its runner is stopped whatever the resource cleanup mode says, and the rest of its resources follow that mode. The job_run.status.cancelled notification fires.
Cancelling a continuous job's run also pauses the job, so no new run takes its place. The console calls it Pause Job.
Cancel is idempotent: a run that already ended is returned unchanged, so an orchestrator can call it on every teardown path, for example from an Airflow on_kill, without checking the status first. A cancelled run can be retried.
The scheduler uses the same path for the replace concurrency policy.
Stopping a run is not instant: its code keeps executing for up to about half a minute while Kubernetes shuts the pod down. Cancelled therefore means LakeSail stopped the run and released its compute, not that nothing happened. Writes your code made before it was stopped stay in place. If the code finished inside that window, the run stays Cancelled, its runner timing and output location are still recorded, and Status Message says so. The same holds for Timeout.
Release a run's kept resources
With the resource cleanup mode retain, or on_success when the run did not succeed, a run keeps its Sail server and namespace after it ends, so you can inspect them, for example with kubectl. They are retained until the end of the run's retention, 24 hours after it ended by default, and then cleaned up. Its Sail server keeps running until then, and that compute is billed. Resource Cleanup on the run's page shows when.
Change retention, on the run's page, moves that time earlier or later, for example to Monday for a run that failed on Friday. It is SetJobRunRetention in the API. Retrying the run starts from the default again.
Clean up now, on the run's page, ends the retention: the Sail server and namespace are removed now, logs included. The run stays on record. It is EndJobRunRetention in the API. Runs cannot be deleted. Once the retention ends, by either route, the page shows the resources as being cleaned up, and the retention can no longer be changed.
Timeout
A job's timeout applies to each run from the moment it reaches Starting, so starting Sail and the runner and installing libraries count towards it, not only your code: leave room for that when you pick a value. A library install that keeps failing retries until the timeout ends the run. With no timeout, a run whose install still keeps failing after 24 hours ends as failed, with the install's last error; a continuous run keeps waiting, because publishing a fix replaces it. It is the timeoutSeconds field on the job, set through the API; a value of 0, the default, means no timeout. A run past it is stopped and ends in Timeout, with the job_run.status.timeout notification. As with cancel, the runner is stopped in every cleanup mode.
Error handling & automatic restarts (Python jobs)
Separately from Retry Run, Automatic restarts (maxRetries in the API) sets how often a failed runner restarts within the same run. A restart runs your code again from the start. What your script does determines whether the runner restarts.
By default, a failure in your code does not restart the runner. If your script raises an exception or exits non-zero, the same file with the same arguments is likely to fail the same way, so the run fails immediately and reports the error. This avoids repeating the failure until the restarts run out, and then reporting only that they ran out instead of the original error. The one exception is Sail running out of memory, described below.
That default is a heuristic, not a guarantee. Code that reads from S3, calls an external service, or depends on anything over a network can fail transiently and would succeed after a restart. Those cases are the exception, so you declare them explicitly.
| What your script does | Outcome | Runner restarts? |
|---|---|---|
Returns normally, or sys.exit() / sys.exit(0) | Succeeded | Not applicable |
Raises SailRunSucceeded | Succeeded | Not applicable |
Raises any exception (ValueError, NameError, …) | Failed, with your traceback | No |
Exits non-zero (sys.exit(1), missing argparse argument) | Failed, exit code reported | No |
| Raises an exception or exits non-zero after the Sail driver or a worker ran out of memory | Failed | Yes |
Raises SailRetryableError | Failed | Yes |
Exits with code 75 | Failed | Yes |
Requesting a restart for a transient failure
Use SailRetryableError for a temporary failure (e.g., a throttled S3 read, a rate-limited API, a flaky upstream, etc.). LakeSail provides this builtin in file and wheel jobs. The following example uses a Python file:
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
try:
df = spark.table("production.sales.orders")
except Exception as e:
# Transient: worth restarting the runner.
raise SailRetryableError(f"could not read input: {e}") from e
df.write.mode("overwrite").parquet(args.output)If raising isn't convenient, such as deep inside a wheel or when shelling out to another process, exit with code 75 instead. That's EX_TEMPFAIL from sysexits.h, the conventional "temporary failure, please retry":
import sys
if not upstream_is_ready():
sys.exit(75) # restart the runnerReserve both for genuinely transient conditions. A bug in your code restarted three times is just the same bug three times, and it delays the error reaching you.
A Python file that does not exist, or a wheel entry point the runner cannot find, fails once as well, with config_error and the file or entry point named in Status Message. A download that fails for a moment, such as a throttled S3 read, restarts the runner.
When Sail runs out of memory
When the Sail driver or a worker runs out of memory, your code gets an exception from the Spark call it was waiting on. LakeSail checks for the memory failure and restarts the runner while Automatic restarts allows, with no code of yours needed. The restart counts as caused by sail_out_of_memory.
A restart runs your code from the start against a fresh Sail. A job that saves its progress as it goes, such as one output per chunk, should skip the chunks it already saved. Work that runs out of memory every time spends every restart, and the run then ends out_of_memory. Its message says which instance type to raise.
Stopping cleanly
When LakeSail stops a runner, your code gets a moment to clean up. That happens when the run is cancelled or passes its timeout, and when the runner is evicted or its node is drained. LakeSail raises SailStopRequested wherever your code is running. It's a builtin like SailRetryableError, so there's nothing to import. Your finally blocks and with statements run as it unwinds:
try:
write_partitions()
except SailStopRequested:
save_checkpoint() # keep it short: the runner is killed when the grace period ends
raiseOr, when the checkpoint means the run needs no restart:
try:
write_partitions()
except SailStopRequested:
save_checkpoint()
raise SailRunSucceeded("checkpoint saved")- Your code has about 30 seconds from the stop until the runner is killed, and about a minute and a half in a continuous job. A cleanup that takes longer is cut off.
- Let the stop propagate.
SailStopRequestedisn't anException, so a broadexcept Exception:doesn't catch it. The run counts as stopped however your code ends after the stop, even if it returns normally or exits with0, because the stop may have cut its work short. Code that carries on is killed when the grace period ends. - If your code has finished its work, or saved enough that the run needs no restart, raise
SailRunSucceededinstead. It's a builtin too, and ends the run as Succeeded after an eviction or a drain. A cancel or a timeout still ends the run as Cancelled or Timeout: LakeSail records that before it stops the runner. - After an eviction or a drain, the runner is restarted and your code runs again from the start. A cancel or a timeout ends the run.
- To stop a continuous job from its own code, raise
SailStopJobinstead. See Stop and resume. In any other job,SailStopJobfails the run asuser_error, without a restart. - Running out of memory gives no warning: the kernel kills the runner at once, and no cleanup runs.
Reading the error
A failure from your own code reports the exception type, its message, and the traceback of your file in Status Message, so the failing line is visible on the run:
ValueError: bad input
File "etl.py", line 12, in <module>Output written to stdout, including df.show(), is not included in Status Message.
Python jobs manage their own output
SQL jobs store results in the network workspace bucket under clusters/ and report the location to LakeSail. Results stay there after the run; delete them in S3 when you no longer need them. A Python job writes wherever your script tells it to (commonly a --output argument), so the run won't show a results path. Check the location your script wrote to.
Worker memory failures
LakeSail checks worker pods to identify memory failures. In a Python job, spark.stop() can remove these pods before LakeSail receives the run's outcome. If the pods are gone, a worker memory failure can appear as user_error instead of out_of_memory, and the runner does not restart. Check the run's logs for the original error.
Why a run ended
A run that ended carries three fields in the API and in webhook payloads, so an orchestrator can decide what to do without parsing Status Message:
terminationReason: the kind of ending, from the table below.exitCode: the exit status of a Python job's code:0on success, the value passed tosys.exit,1for an uncaught exception,75forSailRetryableError, or137when the runner was killed for memory. When the Sail engine is the one killed, the code's own exit status stays. A SQL job has none of its own, so it has one only when its runner died without reporting, such as a crash in a native library or a kill, and then it is the runner's. A run that failed before its code started has none.attempt:1, plus one for each Retry Run.
The API also returns restarts: how often the runner was restarted automatically in this attempt, and why it last was. For a continuous job it counts every restart of the code, including those asked for with Restart.
terminationReason | Meaning | What to do |
|---|---|---|
succeeded | The job's code finished. | Nothing. |
user_error | The job's code failed: an exception, a non-zero exit, or a SQL error. | Fix the code or its input. A retry fails the same way. |
config_error | The runner could not start with the job's settings, for example libraries that installed but were rejected because one replaces a package the platform runs on, or a script or wheel entry point that cannot be found. | Fix the job's configuration. |
timeout | The run passed its timeout. | Raise the timeout or make the job faster. |
cancelled | Someone cancelled the run. | Nothing, or retry it. |
out_of_memory | The runner, the Sail driver or a Sail worker was killed or evicted for using too much memory. The run's message says which, and which instance type to raise. | Use a compute profile with a larger instance type, or process less at a time. |
unschedulable | The cluster had no room for the runner. | Retry later, or free or add capacity. |
infra_error | The platform or the cluster failed, not the job. | Retry the run. |
superseded | A newer version of a continuous job replaced the run. | Nothing. |
stopped_by_job | A continuous job's code raised SailStopJob, which also paused the job. | Fix what made it stop, then resume the job. |
unknown | The runner ended without saying why, for example after its restarts ran out. | Read the logs, then retry. |
When a run misbehaves
Read the run's Status Message and its logs first; application errors such as SQL syntax problems, missing columns, and Python exceptions surface there. For runs that fail to start, hang, time out, or fire at the wrong time, see the job-run entries in Troubleshooting.
When to retry or create a run
- Retry Run: rerun the latest failed, timed-out, or cancelled run under the same run ID and version.
- Run Job: create a separate run from the job's current version.
- ReleaseJobRun: move an intentionally held API run from Pending to Ready.
- CancelJobRun: stop a run that has not finished.
Use Retry Run for a transient failure when preserving the same run identity matters. Use Run Job when you want the previous attempt to remain a separate record or the job has changed.
API reference
- Job runs:
CreateJobRun,DescribeJobRun,RetryJobRun,ReleaseJobRun,CancelJobRun,RestartJobRun,SetJobRunRetention,EndJobRunRetention.