diff --git a/lib/xgit.ex b/lib/xgit.ex index 8e46739..3a40afb 100644 --- a/lib/xgit.ex +++ b/lib/xgit.ex @@ -9,13 +9,6 @@ defmodule Xgit do """ @impl true def start(_type, _args) do - children = [ - {ConCache, - name: :xgit_file_snapshot, - ttl_check_interval: :timer.seconds(1), - global_ttl: :timer.seconds(5)} - ] - - Supervisor.start_link(children, strategy: :one_for_one) + Supervisor.start_link([], strategy: :one_for_one) end end diff --git a/lib/xgit/util/file_snapshot.ex b/lib/xgit/util/file_snapshot.ex deleted file mode 100644 index e054eed..0000000 --- a/lib/xgit/util/file_snapshot.ex +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright (C) 2010, Google Inc. -# and other copyright owners as documented in the project's IP log. -# -# Elixir adaptation from jgit file: -# org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java -# -# Copyright (C) 2019, Eric Scouten -# -# This program and the accompanying materials are made available -# under the terms of the Eclipse Distribution License v1.0 which -# accompanies this distribution, is reproduced below, and is -# available at http://www.eclipse.org/org/documents/edl-v10.php -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# - Neither the name of the Eclipse Foundation, Inc. nor the -# names of its contributors may be used to endorse or promote -# products derived from this software without specific prior -# written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND -# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -defmodule Xgit.Util.FileSnapshot do - @moduledoc false - # Caches when a file was last read, making it possible to detect future edits. - - # This object tracks the last modified time of a file. Later during an - # invocation of `modified?/2` the object will return `true` if the file may have - # been modified and should be re-read from disk. - - # A snapshot does not "live update" when the underlying filesystem changes. - # Callers must poll for updates by periodically invoking `modified?/2`. - - # To work around the "racy git" problem (where a file may be modified multiple - # times within the granularity of the filesystem modification clock) this class - # may return `true` from `modified?/2` if the last modification time of the - # file is less than 3 seconds ago. - - import Xgit.Util.ForceCoverage - - @typedoc ~S""" - Cache for when a file was last saved. - - ## Struct Members - - * `last_modified`: Last observed modification time of the path. - * `ref`: Reference into a cache for when this file was last read. - """ - @type t :: %__MODULE__{last_modified: integer | :missing | :dirty, ref: reference | nil} - - @enforce_keys [:last_modified, :ref] - defstruct [:last_modified, :ref] - - @doc ~S""" - An `Xgit.Util.FileSnapshot` that is considered to always be modified. - - This instance is useful for application code that wants to lazily read a - file, but only after `modified?/2` gets invoked. This snapshot instance - contains only invalid status information. - """ - @spec dirty() :: t - def dirty, do: %__MODULE__{last_modified: :dirty, ref: nil} - - @doc ~S""" - An `Xgit.Util.FileSnapshot` that is clean if the file does not exist. - - This instance is useful if the application wants to consider a missing - file to be clean. `modified?/2` will return `false` if the file path - does not exist. - """ - @spec missing_file :: t - def missing_file, do: %__MODULE__{last_modified: :missing, ref: nil} - - @doc ~S""" - Record a snapshot for a specific file path. - - This function should be invoked before the file is accessed. - """ - @spec save(path :: Path.t()) :: t - def save(path) when is_binary(path) do - last_modified = last_modified_time(path) - - ref = make_ref() - record_time_for_ref(ref) - - %__MODULE__{last_modified: last_modified, ref: ref} - end - - @doc ~S""" - Return `true` if the path may have been modified since the snapshot was saved. - """ - @spec modified?(snapshot :: t, path :: Path.t()) :: boolean - def modified?(%__MODULE__{last_modified: last_modified, ref: ref}, path) - when is_binary(path) and is_integer(last_modified) and is_reference(ref) do - curr_last_modified = last_modified_time(path) - modified_impl?(curr_last_modified, last_modified, ref) - end - - def modified?(%__MODULE__{last_modified: :dirty}, _path), do: cover(true) - def modified?(%__MODULE__{last_modified: :missing}, path), do: File.exists?(path) - - defp last_modified_time(path) when is_binary(path) do - path - |> File.stat!(time: :posix) - |> extract_last_modified_time() - end - - defp extract_last_modified_time(%{mtime: lmt}) when is_integer(lmt), do: lmt - - @doc ~S""" - Update this snapshot when the content hasn't changed. - - If the caller gets `true` from `modified?/2`, re-reads the content, discovers - the content is identical, it can use to make a future call to `modified?/2` - return `false`. - - A typical invocation looks something like this: - - ``` - new_snapshot = - if FileSnapshot.modified?(snapshot, path) do - other = FileSnapshot.save(path) - if old_content_matches_new_content? and snapshot.last_modified == other.last_modified do - FileSnapshot.set_clean(snapshot, other) - else - snapshot - end - else - snapshot - end - ``` - - ## Return Value - - `:ok` - """ - @spec set_clean(snapshot :: t, other :: t) :: :ok - def set_clean(sanpshot, other) - - def set_clean( - %__MODULE__{last_modified: last_modified, ref: ref}, - %__MODULE__{ref: other_ref} - ) do - other_last_read = ConCache.get(:xgit_file_snapshot, other_ref) - - if not_racy_clean?(last_modified, other_last_read) do - ConCache.delete(:xgit_file_snapshot, ref) - else - record_time_for_ref(ref, not_racy_clean?(last_modified, other_last_read)) - end - - cover :ok - end - - defp modified_impl?(file_last_modified, last_modified, ref) do - last_read_time = ConCache.get(:xgit_file_snapshot, ref) - - if last_modified == file_last_modified do - modified_impl_race?(file_last_modified, last_read_time) - else - cover true - end - end - - # There's a potential race condition in which the file was modified at roughly - # the same time as the last time we read the modification time. If these two - # events are too close together, we have to assume the file is modified. - - defp modified_impl_race?(_file_last_modified, x) when x == false or x == nil do - # We have already determined the last read was far enough - # after the last modification that any new modifications - # are certain to change the last modified time. - cover false - end - - defp modified_impl_race?(file_last_modified, last_read_time) do - if not_racy_clean?(file_last_modified, last_read_time) do - # Timestamp from last read is far enough from file modification - # date that we can be confident that the file has not changed - # by virtue of the timestamp. - cover false - else - # We last read this path too close to its last observed - # modification time. We may have missed a modification. - # Scan again, to ensure we still see the same state. - cover true - end - end - - defp not_racy_clean?(last_modified_time, last_read_time) do - last_read_time - last_modified_time >= 3 - # The last modified time granularity of FAT filesystems is 2 seconds. - # Using 3 seconds here provides a reasonably high assurance that - # a modification was not missed. - end - - defp record_time_for_ref(ref, time \\ :os.system_time(:second)) when is_reference(ref), - do: ConCache.put(:xgit_file_snapshot, ref, time) - - defimpl String.Chars do - alias Xgit.Util.FileSnapshot - - @impl true - def to_string(%FileSnapshot{last_modified: :dirty}), do: cover("DIRTY") - - def to_string(%FileSnapshot{last_modified: :missing}), do: cover("MISSING_FILE") - - def to_string(%FileSnapshot{last_modified: last_modified, ref: ref}) do - last_read_time = ConCache.get(:xgit_file_snapshot, ref) - "FileSnapshot[modified: #{last_modified}, read: #{last_read_time}]" - end - end -end diff --git a/mix.exs b/mix.exs index 11c9f00..54f30c6 100644 --- a/mix.exs +++ b/mix.exs @@ -26,7 +26,6 @@ defmodule Xgit.MixProject do defp deps do [ {:benchee, "~> 1.0", only: :dev}, - {:con_cache, "~> 0.13"}, {:credo, "~> 1.1", only: [:dev, :test]}, {:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false}, {:excoveralls, "~> 0.11", only: :test}, diff --git a/mix.lock b/mix.lock index 88fef95..f9e9aba 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,6 @@ "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, optional: false]}]}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, optional: false]}]}, - "con_cache": {:hex, :con_cache, "0.14.0", "863acb90fa08017be3129074993af944cf7a4b6c3ee7c06c5cd0ed6b94fbc223", [:mix], []}, "credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], []}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/xgit/util/file_snapshot_test.exs b/test/xgit/util/file_snapshot_test.exs deleted file mode 100644 index db4850f..0000000 --- a/test/xgit/util/file_snapshot_test.exs +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright (C) 2010, Robin Rosenberg -# and other copyright owners as documented in the project's IP log. -# -# Elixir adaptation from jgit file: -# org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileSnapshotTest.java -# -# Copyright (C) 2019, Eric Scouten -# -# This program and the accompanying materials are made available -# under the terms of the Eclipse Distribution License v1.0 which -# accompanies this distribution, is reproduced below, and is -# available at http://www.eclipse.org/org/documents/edl-v10.php -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or -# without modification, are permitted provided that the following -# conditions are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# - Neither the name of the Eclipse Foundation, Inc. nor the -# names of its contributors may be used to endorse or promote -# products derived from this software without specific prior -# written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND -# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -defmodule Xgit.Util.FileSnapshotTest do - use ExUnit.Case, async: true - - alias Xgit.Util.FileSnapshot - - setup do - Temp.track!() - temp_file_path = Temp.mkdir!(prefix: "tmp_") - {:ok, trash: temp_file_path} - end - - defp wait_next_sec(f) when is_binary(f) do - %{mtime: initial_last_modified} = File.stat!(f, time: :posix) - wait_next_sec(f, initial_last_modified) - end - - defp wait_next_sec(f, initial_last_modified) do - time_now = :os.system_time(:second) - - if time_now <= initial_last_modified do - Process.sleep(100) - wait_next_sec(f, initial_last_modified) - end - end - - test "missing_file/0", %{trash: trash} do - missing = FileSnapshot.missing_file() - path = Temp.path!() - - refute FileSnapshot.modified?(missing, path) - - f1 = create_file!(trash, "missing") - assert FileSnapshot.modified?(missing, f1) - - assert to_string(missing) == "MISSING_FILE" - end - - test "actually is modified (trivial case)", %{trash: trash} do - f1 = create_file!(trash, "simple") - wait_next_sec(f1) - - save = FileSnapshot.save(f1) - append!(f1, 'x') - - wait_next_sec(f1) - - assert FileSnapshot.modified?(save, f1) == true - - assert String.starts_with?(to_string(save), "FileSnapshot") - end - - test "new file without significant wait", %{trash: trash} do - f1 = create_file!(trash, "newfile") - Process.sleep(1500) - - save = FileSnapshot.save(f1) - assert FileSnapshot.modified?(save, f1) == true - end - - test "new file without wait", %{trash: trash} do - # Same as above but do not wait at all. - - f1 = create_file!(trash, "newfile") - wait_next_sec(f1) - - save = FileSnapshot.save(f1) - assert FileSnapshot.modified?(save, f1) == true - end - - test "new file and wait 3 sec", %{trash: trash} do - f1 = create_file!(trash, "newfile") - - Process.sleep(3000) - - save = FileSnapshot.save(f1) - assert FileSnapshot.modified?(save, f1) == false - end - - test "dirty snapshot is always dirty", %{trash: trash} do - f1 = create_file!(trash, "newfile") - wait_next_sec(f1) - - dirty = FileSnapshot.dirty() - assert FileSnapshot.modified?(dirty, f1) == true - - assert to_string(dirty) == "DIRTY" - end - - describe "set_clean/2" do - test "without delay", %{trash: trash} do - f1 = create_file!(trash, "newfile") - wait_next_sec(f1) - - save = FileSnapshot.save(f1) - assert FileSnapshot.modified?(save, f1) == true - - # an abuse of the API, but best we can do - FileSnapshot.set_clean(save, save) - assert FileSnapshot.modified?(save, f1) == false - end - - test "with (faked) delay", %{trash: trash} do - f1 = create_file!(trash, "newfile") - wait_next_sec(f1) - - save = FileSnapshot.save(f1) - assert FileSnapshot.modified?(save, f1) == true - - modified_earlier = %{save | last_modified: save.last_modified - 10} - FileSnapshot.set_clean(modified_earlier, save) - assert FileSnapshot.modified?(modified_earlier, f1) == true - end - end - - defp create_file!(trash, leaf_name) when is_binary(trash) and is_binary(leaf_name) do - path = Path.expand(leaf_name, trash) - File.touch!(path) - path - end - - defp append!(path, b) when is_binary(path) and is_list(b), do: File.write!(path, b, [:append]) -end