defmodule Tarakan.RepositoryCode.InstrumentedGitHubClient do @moduledoc false @behaviour Tarakan.GitHubClient alias Tarakan.GitHubStub def child_spec(opts) do %{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}} end def start_link(_opts) do Agent.start_link( fn -> %{ calls: %{}, notify: nil, block_once: MapSet.new(), visibility: :public } end, name: __MODULE__ ) end def configure(opts) do Agent.update(__MODULE__, fn state -> %{ state | notify: Keyword.get(opts, :notify, state.notify), block_once: MapSet.new(Keyword.get(opts, :block_once, [])), visibility: Keyword.get(opts, :visibility, state.visibility) } end) end def count(kind), do: Agent.get(__MODULE__, &Map.get(&1.calls, kind, 0)) @impl true def fetch_repository(owner, name, opts \\ []) do record_call(:repository) case Agent.get(__MODULE__, & &1.visibility) do :public -> GitHubStub.fetch_repository(owner, name, opts) :private -> # A visibility change always rotates the upstream ETag, so a # conditional request can never mask it with a 304. with {:ok, metadata} <- GitHubStub.fetch_repository(owner, name) do {:ok, %{metadata | private: true, visibility: "private"}} end :not_found -> {:error, :not_found} {:github_id, github_id} -> with {:ok, metadata} <- GitHubStub.fetch_repository(owner, name) do {:ok, %{metadata | github_id: github_id}} end end end @impl true def fetch_repository_by_id(github_id) do record_call(:repository_by_id) case Agent.get(__MODULE__, & &1.visibility) do :public -> GitHubStub.fetch_repository_by_id(github_id) :private -> with {:ok, metadata} <- GitHubStub.fetch_repository_by_id(github_id) do {:ok, %{metadata | private: true, visibility: "private"}} end :not_found -> {:error, :not_found} {:github_id, _forced_id} -> GitHubStub.fetch_repository_by_id(github_id) end end @impl true def fetch_commit(owner, name, sha) do record_call(:commit) GitHubStub.fetch_commit(owner, name, sha) end @impl true def fetch_branch_head(owner, name, branch) do record_call(:head) GitHubStub.fetch_branch_head(owner, name, branch) end @impl true def list_branches(owner, name) do record_call(:branches) GitHubStub.list_branches(owner, name) end @impl true def fetch_tree(owner, name, tree_sha, recursive) do record_call(:tree) GitHubStub.fetch_tree(owner, name, tree_sha, recursive) end @impl true def fetch_text_blob(owner, name, blob_sha) do record_call(:blob) GitHubStub.fetch_text_blob(owner, name, blob_sha) end defp record_call(kind) do {notify, block?} = Agent.get_and_update(__MODULE__, fn state -> block? = MapSet.member?(state.block_once, kind) next_state = %{ state | calls: Map.update(state.calls, kind, 1, &(&1 + 1)), block_once: MapSet.delete(state.block_once, kind) } {{state.notify, block?}, next_state} end) if block? and is_pid(notify) do send(notify, {:upstream_blocked, kind, self()}) receive do {:release_upstream, ^kind} -> :ok after 5_000 -> :ok end end end end