defmodule Mix.Tasks.Rindle.AbortIncompleteUploads do @shortdoc "Transition timed-out upload sessions to expired" @moduledoc """ Transitions timed-out upload sessions through the abort half of Rindle's maintenance lane. This is a prerequisite for `mix rindle.cleanup_orphans`, which only removes sessions that are already in the `expired` state. Running both tasks in sequence provides a safe, two-step cleanup lane: 1. `mix rindle.abort_incomplete_uploads` — mark timed-out sessions as expired and cancel resumable sessions remotely when needed. 2. `mix rindle.cleanup_orphans` — delete expired sessions and their objects. ## Usage mix rindle.abort_incomplete_uploads ## Exit codes * `0` — all timed-out sessions were successfully transitioned. * `1` — one or more sessions could not be transitioned (errors are logged and counted in the output). ## Examples # Standard usage in a cron/CI pipeline mix rindle.abort_incomplete_uploads && mix rindle.cleanup_orphans ## Notes Sessions that are already in a terminal state (`completed`, `expired`, `failed`) are not touched by the first-pass timeout scan. Timed-out resumable sessions are cancelled only from this abort lane, and retryable resumable cancel failures are retried here until they converge or exhaust worker attempts. """ use Mix.Task alias Rindle.Ops.UploadMaintenance @requirements ["app.start"] @impl Mix.Task def run(argv) do {_opts, _args, _invalid} = OptionParser.parse(argv, strict: []) Mix.shell().info("Aborting incomplete uploads past their TTL...") case UploadMaintenance.abort_incomplete_uploads([]) do {:ok, report} -> print_abort_report(report) maybe_exit_nonzero(report.abort_errors) {:error, reason} -> Mix.shell().error("Abort task failed: #{inspect(reason)}") exit({:shutdown, 1}) end end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp print_abort_report(report) do Mix.shell().info(""" Incomplete-upload abort complete: Sessions found: #{report.sessions_found} Sessions aborted: #{report.sessions_aborted} Resumable aborts: #{report.resumable_aborts} Multipart aborts: #{report.multipart_aborts} Presigned PUT aborts: #{report.presigned_put_aborts} Abort errors: #{report.abort_errors} """) end # Exits 0 when no errors occurred. defp maybe_exit_nonzero(0), do: :ok # Exits non-zero so CI and cron scripts can detect problems. defp maybe_exit_nonzero(error_count) do Mix.shell().error("#{error_count} session(s) could not be transitioned to expired.") exit({:shutdown, 1}) end end