From 5e653c8840b671ca3dcffbe23c7b31d6b9f67713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Jime=CC=81nez?= Date: Sat, 7 Jun 2025 13:26:10 +0200 Subject: [PATCH 1/3] Add support for custom lock value in Redlock and test coverage - Introduce `lock/3` allowing to specify a custom lock value (e.g. node id) - Store value in Redis for easier debugging and ownership tracing - Add test covering custom value lock and unlock --- lib/redlock.ex | 25 ++++++++++++++++++++++++- lib/redlock/executor.ex | 4 ++++ test/redlock_test.exs | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/redlock.ex b/lib/redlock.ex index 85536f7..81d604a 100644 --- a/lib/redlock.ex +++ b/lib/redlock.ex @@ -22,7 +22,26 @@ defmodule Redlock do end - Or you can use `transaction` function + You can optionally assign a custom value (for example, your node name, hostname, or any unique identifier per process) to the lock. + This value is stored as the lock value in Redis, making it easy to identify and debug which node or process currently owns the lock. + + resource = "example_key:#{user_id}" + lock_exp_sec = 10 + value = Atom.to_string(Node.self()) + + case Redlock.lock(resource, lock_exp_sec, value) do + + {:ok, mutex} -> + # some other code which write and read on RDBMS, KVS or other storage + # call unlock finally + Redlock.unlock(resource, mutex) + + :error -> + Logger.error "failed to lock resource. maybe redis connection got trouble." + {:error, :system_error} + end + + Or you can use the `transaction` function: def my_function() do # do something, and return {:ok, :my_result} or {:error, :my_error} @@ -162,6 +181,10 @@ defmodule Redlock do Redlock.Executor.lock(resource, ttl) end + def lock(resource, ttl, value) do + Redlock.Executor.lock(resource, ttl, value) + end + def unlock(resource, value) do Redlock.Executor.unlock(resource, value) end diff --git a/lib/redlock/executor.ex b/lib/redlock/executor.ex index ba719a9..1e23d9f 100644 --- a/lib/redlock/executor.ex +++ b/lib/redlock/executor.ex @@ -10,6 +10,10 @@ defmodule Redlock.Executor do do_lock(resource, ttl, random_value(), 0, FastGlobal.get(:redlock_conf)) end + def lock(resource, ttl, value) do + do_lock(resource, ttl, value, 0, FastGlobal.get(:redlock_conf)) + end + def unlock(resource, value) do config = FastGlobal.get(:redlock_conf) diff --git a/test/redlock_test.exs b/test/redlock_test.exs index 7302b49..e3b3719 100644 --- a/test/redlock_test.exs +++ b/test/redlock_test.exs @@ -77,4 +77,22 @@ defmodule RedlockTest do # extending a lock that is not held should fail assert Redlock.extend("not-locked", "not-mutex", 10) == :error end + + test "lock with custom value" do + key = "with_value" + value = "node-12345" + + # Acquire lock with custom value + assert {:ok, ^value} = Redlock.lock(key, 10, value) + + # Attempt to lock again with different value should fail + assert :error == Redlock.lock(key, 10, "node-other") + + # Release lock with value + assert :ok == Redlock.unlock(key, value) + + # Now should be able to acquire again + assert {:ok, ^value} = Redlock.lock(key, 10, value) + assert :ok == Redlock.unlock(key, value) + end end From f6f011d2a29a1204f6bb9785ed435baa29132ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Jime=CC=81nez?= Date: Sat, 28 Feb 2026 12:17:15 +0100 Subject: [PATCH 2/3] chore(redlock): add credo linter, precommit alias, and stricter CI Add credo dependency with --strict in CI, warnings_as_errors for compile, and precommit mix alias. Co-Authored-By: Claude Opus 4.6 --- .credo.exs | 108 +++++++++++++++++++++++++++++++++++++ .github/workflows/main.yml | 3 +- mix.exs | 11 +++- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 .credo.exs diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..2784eaf --- /dev/null +++ b/.credo.exs @@ -0,0 +1,108 @@ +%{ + configs: [ + %{ + name: "default", + files: %{ + included: [ + "lib/" + ], + excluded: [ + ~r"/_build/", + ~r"/deps/", + ~r"/node_modules/" + ] + }, + plugins: [], + requires: [], + strict: true, + parse_timeout: 5000, + color: true, + checks: %{ + enabled: [ + # + # Consistency + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + # Design + # + {Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 1]}, + {Credo.Check.Design.TagTODO, [exit_status: 0]}, + {Credo.Check.Design.TagFIXME, []}, + + # + # Readability + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, [priority: :low]}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + + # + # Refactoring + # + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, [max_complexity: 15]}, + {Credo.Check.Refactor.FunctionArity, [max_arity: 6]}, + {Credo.Check.Refactor.LongQuoteBlocks, [max_line_count: 500]}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, [max_nesting: 4]}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + # Warnings + # + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []} + ], + disabled: [ + # Disabled: macros generate code that legitimately doesn't use all params + {Credo.Check.Warning.UnusedFunctionReturnHelper, []}, + # Disabled: pipe vs no pipe is a style choice in our macros + {Credo.Check.Refactor.PipeChainStart, []} + ] + } + } + ] +} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aa92393..b375c55 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,7 @@ jobs: - run: mix deps.compile - run: mix format --check-formatted - run: mix deps.unlock --check-unused + - run: mix credo --strict test: runs-on: ${{ matrix.os }} @@ -66,5 +67,5 @@ jobs: restore-keys: ${{ matrix.os }}-otp_${{ matrix.otp }}-elixir_${{ matrix.elixir }}-mix_ - run: mix deps.get --only test - run: mix deps.compile - - run: mix compile + - run: mix compile --warnings-as-errors - run: mix test diff --git a/mix.exs b/mix.exs index c05fe3a..7fcd545 100644 --- a/mix.exs +++ b/mix.exs @@ -10,6 +10,8 @@ defmodule Redlock.Mixfile do version: @version, package: package(), start_permanent: Mix.env() == :prod, + elixirc_options: [warnings_as_errors: true], + aliases: aliases(), deps: deps(), docs: docs() ] @@ -28,7 +30,8 @@ defmodule Redlock.Mixfile do {:redix, "~> 1.3"}, {:poolboy, "~> 1.5"}, {:fastglobal, "~> 1.0.0"}, - {:ex_hash_ring, "~> 3.0"} + {:ex_hash_ring, "~> 3.0"}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false} ] end @@ -48,6 +51,12 @@ defmodule Redlock.Mixfile do ] end + defp aliases do + [ + precommit: ["compile", "credo --strict", "deps.unlock --unused", "format", "test"] + ] + end + defp package() do [ description: "Redlock (Redis Distributed Lock) implementation", From ed9b44c09e2d54ef5d2e490d7f67b8010ea7b6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Jime=CC=81nez?= Date: Sat, 21 Mar 2026 21:39:21 +0100 Subject: [PATCH 3/3] chore: add MCP server configuration Co-Authored-By: Claude Opus 4.6 (1M context) --- .mcp.json | 1 + 1 file changed, 1 insertion(+) create mode 120000 .mcp.json diff --git a/.mcp.json b/.mcp.json new file mode 120000 index 0000000..68df7ad --- /dev/null +++ b/.mcp.json @@ -0,0 +1 @@ +../../.mcp.json \ No newline at end of file