Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 67 additions & 6 deletions .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ allowed_imports = [
"apps/*/lib/",
"apps/*/src/",
"apps/*/test/",
"apps/*/web/"
"apps/*/web/",
"mix.exs",
".credo.exs"
],
excluded: [~r"_build/", ~r"deps/"]
},
plugins: [],
strict: true,
parse_timeout: 10000,
parse_timeout: 10_000,
color: true,
checks: [

Expand All @@ -40,14 +42,73 @@ allowed_imports = [
{BlitzCredoChecks.SetWarningsAsErrorsInTest, false},
{BlitzCredoChecks.DocsBeforeSpecs, []},
{BlitzCredoChecks.DoctestIndent, []},
{BlitzCredoChecks.NoAsyncFalse, []},
# Superseded by MikaCredoRules.AsyncTrueRequired: the choice must be explicit, and a
# deliberate async: false is allowed.
{BlitzCredoChecks.NoAsyncFalse, false},
{BlitzCredoChecks.NoDSLParentheses, []},
{BlitzCredoChecks.NoIsBitstring, []},
{BlitzCredoChecks.StrictComparison, []},
{BlitzCredoChecks.StrictComparison, false},
{BlitzCredoChecks.UseStream, []},
{BlitzCredoChecks.LowercaseTestNames, []},
{BlitzCredoChecks.ImproperImport, allowed_modules: allowed_imports},

# MikaCredoRules (supersedes BlitzCredoChecks.StrictComparison, TagTODO, TagFIXME)
{MikaCredoRules.AsyncTrueRequired, []},
{MikaCredoRules.CredoConfigNamedDefault, []},
{MikaCredoRules.DistributionRequiresBuckets, []},
{MikaCredoRules.EnsureLoadedBeforeExported, []},
# lib/cache/ets.ex: opts_definition/1 is a NimbleOptions :custom validator, whose contract
# is {:error, String.t()}
{MikaCredoRules.ErrorMessageRequired, [excluded_paths: ["_test.exs", "test/", "lib/cache/ets.ex"]]},
{MikaCredoRules.ExceptionNamesEndInError, []},
{MikaCredoRules.GenServerRequiresHandleContinue, []},
{MikaCredoRules.LoggerModulePrefixAndInspect, []},
{MikaCredoRules.LowercaseErrorMessages, []},
{MikaCredoRules.NoAccessOnStructSubject, []},
{MikaCredoRules.NoApplicationEnvOutsideConfig, []},
{MikaCredoRules.NoAtomStringKeyFallback, []},
{MikaCredoRules.NoBarePatternMatchOnFallible, []},
{MikaCredoRules.NoBinaryPatternForStringPrefix, []},
{MikaCredoRules.NoBlanketRescue, []},
{MikaCredoRules.NoBooleanLiteralComparison, []},
{MikaCredoRules.NoCondElseAtom, []},
{MikaCredoRules.NoDoPrefixedHelper, []},
{MikaCredoRules.NoForWithDiscardedResult, []},
{MikaCredoRules.NoHardcodedSecretLiterals, []},
{MikaCredoRules.NoIOANSIRawEscapes, []},
{MikaCredoRules.NoIdentityRewrap, []},
{MikaCredoRules.NoIntermediateSingleUseVariable, []},
{MikaCredoRules.NoKernelPrefix, []},
{MikaCredoRules.NoLengthZeroComparison, []},
{MikaCredoRules.NoMixEnvAtRuntime, []},
{MikaCredoRules.NoMockingLibraries, []},
{MikaCredoRules.NoNilComparison, []},
{MikaCredoRules.NoPipeIntoControlFlow, []},
{MikaCredoRules.NoProcessSleepInTests, []},
{MikaCredoRules.NoSelfSendZeroDelay, []},
{MikaCredoRules.NoSingleLetterVariables, []},
{MikaCredoRules.NoTaskAsyncInGenServer, []},
{MikaCredoRules.NoTruthyAndOr, []},
{MikaCredoRules.NoUnboundBlockAssignment, []},
{MikaCredoRules.NoUnsupervisedTaskStart, []},
{MikaCredoRules.NoVacuousAssert, []},
{MikaCredoRules.NoWordSigilLists, []},
{MikaCredoRules.RefuteOverAssertNot, []},
{MikaCredoRules.SingleModulePerFile,
[excluded_paths: ["test/", "test/support/", "_test.exs", "cache_test_modules.ex"]]},
{MikaCredoRules.StrictEquality, []},
{MikaCredoRules.TaskAsyncStreamRequiresTimeout, []},
{MikaCredoRules.TestOnlyDepsScoped,
[test_only_packages: [:credo, :blitz_credo_checks, :mika_credo_rules, :dialyxir, :excoveralls, :ex_doc]]},
{MikaCredoRules.TodosNeedTickets, []},

# MikaCredoRules that target consumers of this library, not the library itself
{MikaCredoRules.CacheOptsNoHardcodedUri, false},
{MikaCredoRules.CacheRequiresSandboxOption, false},
{MikaCredoRules.NoDirectErlangRpc, false},
{MikaCredoRules.NoRawEts, false},
{MikaCredoRules.NoReimplementedHelper, false},

# Consistency Checks
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
Expand All @@ -60,8 +121,8 @@ allowed_imports = [
{Credo.Check.Design.AliasUsage, false},

# No outstanding TODOs
{Credo.Check.Design.TagTODO, []},
{Credo.Check.Design.TagFIXME, []},
{Credo.Check.Design.TagTODO, false},
{Credo.Check.Design.TagFIXME, false},

# # Readability Checks
{Credo.Check.Readability.AliasOrder, false},
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

## Bug Fixes

- fix(refresh_ahead, hash_ring): the background refresh task and the read-repair delete task now run under a `Task.Supervisor` inside the strategy's own supervision tree instead of an unsupervised `Task.start/1`.
- fix(hash_ring): an exception raised by the RPC module during read-repair is logged at warning level before the node is treated as unavailable, instead of being swallowed.
- fix(hash_ring): `Cache.HashRing.RingMonitor` seeds its history table and subscribes to node events from `handle_continue/2` rather than blocking in `init/1`.
- fix: `:compression_level` is reachable. It was unusable on every path — no adapter declares it, so `NimbleOptions` rejected it on compile-time adapter opts, and it resolved to `nil` before it could reach the encoder otherwise. It is now an option on the `use Cache` line (`compression_level: 6`), it is taken off the adapter opts before they are validated so the `opts: [compression_level: 6]` spelling works too, and it is never handed to the adapter. Setting it forces encoding on adapters that hold terms natively — asking for compression is asking for bytes. A cache using a strategy adapter raises at compile time rather than ignoring the option.
- fix: `Cache.ConCache.get_or_store/3` followed by `get/1` no longer raises. `get_or_store/3` writes through ConCache directly, bypassing the encode in `put/3`, so the matching `get/1` tried to `binary_to_term/1` a raw term.
- fix: a binary value wrapped in braces but not valid JSON (eg `"{oops}"`) no longer raises `Jason.DecodeError` on read. `Cache.TermEncoder.decode/1` used `Jason.decode!/1`, and now falls back to returning the binary unchanged.
Expand Down
16 changes: 10 additions & 6 deletions lib/cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@ defmodule Cache do
defp adapter_options!({module, fun, args}), do: apply(module, fun, args)

match?({_, _}, @adapter_opts) ->
defp adapter_options!({app, key}), do: Application.fetch_env!(app, key)
defp adapter_options!({app, key}), do: Cache.Config.fetch_env!(app, key)

is_atom(@adapter_opts) and not is_nil(@adapter_opts) ->
defp adapter_options!(app_name) when is_atom(app_name),
do: Application.fetch_env!(app_name, __MODULE__)
do: Cache.Config.fetch_env!(app_name, __MODULE__)

is_function(@adapter_opts, 0) ->
defp adapter_options!(fun) when is_function(fun, 0), do: fun.()
Expand Down Expand Up @@ -211,7 +211,9 @@ defmodule Cache do
@cache_name
|> @cache_strategy_module.get(key, @cache_strategy_config, adapter_options())
|> handle_adapter_result(:get, @cache_name)
|> case do

result =
case result do
{:ok, nil} = res ->
:telemetry.execute([:elixir_cache, :cache, :get, :miss], %{count: 1}, %{
cache_name: @cache_name
Expand Down Expand Up @@ -357,11 +359,11 @@ defmodule Cache do
defp adapter_options!({module, fun, args}), do: apply(module, fun, args)

match?({_, _}, @adapter_opts) ->
defp adapter_options!({app, key}), do: Application.fetch_env!(app, key)
defp adapter_options!({app, key}), do: Cache.Config.fetch_env!(app, key)

is_atom(@adapter_opts) and not is_nil(@adapter_opts) ->
defp adapter_options!(app_name) when is_atom(app_name),
do: Application.fetch_env!(app_name, __MODULE__)
do: Cache.Config.fetch_env!(app_name, __MODULE__)

is_function(@adapter_opts, 0) ->
defp adapter_options!(fun) when is_function(fun, 0), do: fun.()
Expand Down Expand Up @@ -426,7 +428,9 @@ defmodule Cache do
@cache_name
|> @cache_adapter.get(key, adapter_options())
|> handle_adapter_result(:get, @cache_name)
|> case do

result =
case result do
{:ok, nil} = res ->
:telemetry.execute([:elixir_cache, :cache, :get, :miss], %{count: 1}, %{
cache_name: @cache_name
Expand Down
4 changes: 2 additions & 2 deletions lib/cache/case_template.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ defmodule Cache.CaseTemplate do
|> Supervisor.which_children()
|> Enum.filter(fn {_id, _pid, _type, modules} ->
is_list(modules) and
Enum.any?(modules, &function_exported?(&1, :cache_name, 0))
Enum.any?(modules, &(Code.ensure_loaded?(&1) and function_exported?(&1, :cache_name, 0)))
end)
|> Enum.flat_map(fn {_id, _pid, _type, modules} ->
Enum.filter(modules, &function_exported?(&1, :cache_name, 0))
Enum.filter(modules, &(Code.ensure_loaded?(&1) and function_exported?(&1, :cache_name, 0)))
end)
end
end
Expand Down
10 changes: 5 additions & 5 deletions lib/cache/con_cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,26 +98,26 @@ defmodule Cache.ConCache do
def put(cache_name, key, _ttl \\ nil, value, _opts \\ [])

def put(cache_name, key, nil, value, opts) do
if is_nil(opts[:dirty?]) or opts[:dirty?] do
if Keyword.get(opts, :dirty?, true) do
ConCache.dirty_put(cache_name, key, value)
else
ConCache.put(cache_name, key, value)
end
rescue
exception ->
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

def put(cache_name, key, ttl, value, opts) do
item = %ConCache.Item{value: value, ttl: ttl}

if is_nil(opts[:dirty?]) or opts[:dirty?] do
if Keyword.get(opts, :dirty?, true) do
ConCache.dirty_put(cache_name, key, item)
else
ConCache.put(cache_name, key, item)
end
rescue
exception ->
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

Expand All @@ -132,7 +132,7 @@ defmodule Cache.ConCache do
def delete(cache_name, key, _opts \\ []) do
ConCache.delete(cache_name, key)
rescue
exception ->
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

Expand Down
2 changes: 2 additions & 0 deletions lib/cache/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ defmodule Cache.Config do
def sandbox_sleep_ms do
Application.get_env(@app, :sandbox_sleep_ms, 50)
end

def fetch_env!(app, key), do: Application.fetch_env!(app, key)
end
10 changes: 5 additions & 5 deletions lib/cache/counter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ defmodule Cache.Counter do
{:error, ErrorMessage.bad_request("integer key #{key} is out of bounds (initial_size: #{size})", %{cache: cache_name, key: key})}
end
rescue
exception ->
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

Expand All @@ -166,7 +166,7 @@ defmodule Cache.Counter do
:counters.add(ref, compute_index(ref, key), value)
:ok
rescue
exception ->
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

Expand All @@ -184,7 +184,7 @@ defmodule Cache.Counter do
:counters.put(ref, compute_index(ref, key), 0)
:ok
rescue
exception ->
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

Expand All @@ -194,7 +194,7 @@ defmodule Cache.Counter do
:counters.add(ref, compute_index(ref, key), step)
:ok
rescue
exception ->
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

Expand All @@ -204,7 +204,7 @@ defmodule Cache.Counter do
:counters.add(ref, compute_index(ref, key), -step)
:ok
rescue
exception ->
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

Expand Down
9 changes: 6 additions & 3 deletions lib/cache/dets.ex
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ defmodule Cache.DETS do
@doc """
Returns the list of objects associated with slot I.
"""
def slot(i) do
:dets.slot(@cache_name, i)
def slot(index) do
:dets.slot(@cache_name, index)
end

@doc """
Expand Down Expand Up @@ -401,7 +401,10 @@ defmodule Cache.DETS do
|> Keyword.drop([:table_name, :file_path])
|> Kernel.++(access: :read_write, file: file_path)

{:ok, _} = :dets.open_file(table_name, opts)
case :dets.open_file(table_name, opts) do
{:ok, _table} -> :ok
{:error, reason} -> exit({:dets_open_file, reason})
end

send(parent, {ref, :ready})
Process.hibernate(Function, :identity, [nil])
Expand Down
25 changes: 14 additions & 11 deletions lib/cache/ets.ex
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,8 @@ defmodule Cache.ETS do
@doc """
Returns the list of objects associated with slot I.
"""
def slot(i) do
@cache_adapter.slot(@cache_name, i)
def slot(index) do
@cache_adapter.slot(@cache_name, index)
end

@doc """
Expand Down Expand Up @@ -621,7 +621,7 @@ defmodule Cache.ETS do
end

defp maybe_compressed(original_opts, validated) do
if Keyword.has_key?(original_opts, :compressed) and validated[:compressed], do: :compressed
if Keyword.has_key?(original_opts, :compressed) && validated[:compressed], do: :compressed
end

defp maybe_kv(original_opts, validated, key) do
Expand Down Expand Up @@ -673,15 +673,15 @@ defmodule Cache.ETS do
if File.exists?(file_path) do
case :ets.file2tab(String.to_charlist(file_path)) do
{:ok, ^table_name} ->
Logger.info("[Cache.ETS] Rehydrated #{table_name} from #{file_path}")
Logger.info("#{__MODULE__}: rehydrated table #{inspect(table_name)} from #{inspect(file_path)}")
:ok

{:ok, _other_name} ->
Logger.warning("[Cache.ETS] File #{file_path} has different table name, creating new table #{table_name}")
Logger.warning("#{__MODULE__}: file #{inspect(file_path)} has a different table name, creating new table #{inspect(table_name)}")
:ets.new(table_name, ets_opts)

{:error, reason} ->
Logger.warning("[Cache.ETS] Failed to rehydrate #{table_name} from #{file_path}: #{inspect(reason)}, creating new table")
Logger.warning("#{__MODULE__}: failed to rehydrate #{inspect(table_name)} from #{inspect(file_path)}, creating new table, reason: #{inspect(reason)}")
:ets.new(table_name, ets_opts)
end
else
Expand All @@ -703,7 +703,7 @@ defmodule Cache.ETS do
end) do
{:ok, _ref} -> :ok
{:error, reason} ->
Logger.error("[Cache.ETS] Failed to setup exit signal handler for #{table_name}: #{inspect(reason)}")
Logger.error("#{__MODULE__}: failed to setup exit signal handler for #{inspect(table_name)}, reason: #{inspect(reason)}")

:ok
end
Expand Down Expand Up @@ -732,7 +732,8 @@ defmodule Cache.ETS do
[] -> {:ok, nil}
end
rescue
e -> {:error, ErrorMessage.internal_server_error(Exception.message(e), %{cache: cache_name, key: key})}
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

@impl Cache
Expand All @@ -742,7 +743,8 @@ defmodule Cache.ETS do

:ok
rescue
e -> {:error, ErrorMessage.internal_server_error(Exception.message(e), %{cache: cache_name, key: key})}
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

@impl Cache
Expand All @@ -752,7 +754,8 @@ defmodule Cache.ETS do

:ok
rescue
e -> {:error, ErrorMessage.internal_server_error(Exception.message(e), %{cache: cache_name, key: key})}
exception in ArgumentError ->
{:error, ErrorMessage.internal_server_error(Exception.message(exception), %{cache: cache_name, key: key})}
end

# ETS-specific module-level functions called via @cache_adapter from the __using__ macro.
Expand Down Expand Up @@ -841,7 +844,7 @@ defmodule Cache.ETS do

def setopts(cache_name, opts), do: :ets.setopts(cache_name, opts)

def slot(cache_name, i), do: :ets.slot(cache_name, i)
def slot(cache_name, index), do: :ets.slot(cache_name, index)

def tab2file(cache_name, filename), do: :ets.tab2file(cache_name, filename)
def tab2file(cache_name, filename, options), do: :ets.tab2file(cache_name, filename, options)
Expand Down
Loading
Loading