lib/openagents/repositories/import_workspace_janitor.ex

58e6347eeb72 · 2 KB

defmodule OpenAgents.Repositories.ImportWorkspaceJanitor do
  @moduledoc "Removes a bounded set of stale, crash-left GitHub import workspaces."

  use GenServer

  require Logger

  @default_interval_ms 15 * 60 * 1_000
  @default_retention_ms 2 * 60 * 60 * 1_000
  @maximum_entries 100
  @workspace_pattern ~r/\Aopenagents-import-[0-9a-f-]{36}-[0-9]+\z/

  def start_link(options) do
    name = Keyword.get(options, :name, __MODULE__)
    GenServer.start_link(__MODULE__, options, name: name)
  end

  @doc "Removes up to 100 stale import workspaces and returns the removal count."
  def sweep(now_ms \\ System.system_time(:millisecond)) do
    root = Application.get_env(:openagents, :repository_import_temp_dir, System.tmp_dir!())

    retention_ms =
      Application.get_env(
        :openagents,
        :repository_import_workspace_retention_ms,
        @default_retention_ms
      )

    case File.ls(root) do
      {:ok, entries} ->
        entries
        |> Enum.filter(&Regex.match?(@workspace_pattern, &1))
        |> Enum.sort()
        |> Enum.take(@maximum_entries)
        |> Enum.count(fn entry ->
          remove_if_stale(Path.join(root, entry), now_ms, retention_ms)
        end)

      {:error, _reason} ->
        0
    end
  end

  @impl true
  def init(options) do
    interval_ms = Keyword.get(options, :interval_ms, @default_interval_ms)
    schedule(interval_ms)
    {:ok, %{interval_ms: interval_ms}}
  end

  @impl true
  def handle_info(:sweep, state) do
    _count = sweep()
    schedule(state.interval_ms)
    {:noreply, state}
  end

  def handle_info(_message, state), do: {:noreply, state}

  defp remove_if_stale(path, now_ms, retention_ms)
       when is_integer(retention_ms) and retention_ms >= 0 do
    case File.lstat(path, time: :posix) do
      {:ok, %{type: :directory, mtime: mtime}} when now_ms - mtime * 1_000 > retention_ms ->
        case File.rm_rf(path) do
          {:ok, _paths} ->
            Logger.info("repository import janitor removed a stale workspace")
            true

          {:error, _reason, _path} ->
            false
        end

      _other ->
        false
    end
  end

  defp remove_if_stale(_path, _now_ms, _retention_ms), do: false

  defp schedule(interval_ms), do: Process.send_after(self(), :sweep, interval_ms)
end