Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More flexible result matching #61

Merged
merged 11 commits into from
Sep 22, 2024

Conversation

nathanalderson
Copy link
Contributor

This PR adds support for more flexible matching on the return value and raised exception for determining whether to retry. Instead of only supporting a list of atoms for each, you can now provide any of the following:

  • An atom (for example: :not_okay, SomeStruct, or CustomError). In this case, the do block will be retried in any of the following cases:
    • The atom itself is returned
    • The atom is returned in the first position of a two-tuple (for example, {:not_okay, _})
    • A struct of that type is returned/raised
  • The special atom :all. In this case, all values/exceptions will be retried.
  • A function (for example: fn val -> val.starts_with("ok") end) or partial function (for example: fn {:error, %SomeStruct{reason: "busy"}} -> true). The function will be called with the return value and the do block will be retried if the function returns a truthy value. If the function returns a falsy value or if no function clause matches, the do block will not be retried.
  • A list of any of the above. The do block will be retried if any of the items in the list matches.

This should address both #54 and #56.


assert elapsed / 1_000 >= 250
assert f.() == :error
assert Regex.scan(~r/running/, capture_log(f)) |> length == 1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to capture the log messages here and elsewhere? I personally find it redundant and a bit noisy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my way of counting the number of executions. Basically just using logs as a side effect that I can count with capture_log. I prefer it over the duration measuring, but I'll use that method if you prefer.

after
result -> result
else
error -> :not_this
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind fixing the warning(s) by renaming this to _error?

{%NotOkay{}, NotOkay},
{%NotOkay{}, [Foo, NotOkay]},
{:not_ok, fn _ -> true end},
{:not_ok, [fn _ -> false end, fn _ -> true end]},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I think this is a great feature, I feel like passing a function or a list of functions to atoms is semantically weird 🤔. The original idea behind atoms is to allowlist things like :error, {:error, foo}, :kaput, etc. Although come to think of it, a more inclusive name for it would be patterns. I think we can add patterns and deprecate atoms if necessary.

Thinking out loud, what if we create a new option called when/when_truthy that accepts one or more functions instead? I personally think it reads better as well, i.e. "retry when a function that accepts the return value evaluates to true".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that atoms is no longer the appropriate name, but we obviously don't want to remove atoms for backward compatibility reasons so I was hoping we could merge this PR and then maybe in a follow-up add an alias for it and deprecate it.

As far as choosing a new name, I think when or patterns are both fine. Personally I wouldn't add a third parameter for functions separate from one for atoms (not sure if that's what you were suggesting). If anything, I would go the opposite direction and combing the rescue_only functionality in with when (or whatever we call it) so you just have a single parameter for deciding when to retry.

README.md Outdated
* The atom is returned in the first position of a two-tuple (for example, `{:not_okay, _}`)
* A struct of that type is returned/raised
* The special atom `:all`. In this case, all values/exceptions will be retried.
* A function (for example: `fn val -> val.starts_with("ok") end`) or partial function (for example: `fn {:error, %SomeStruct{reason: "busy"}} -> true`). The function will be called with the return value and the `do` block will be retried if the function returns a truthy value. If the function returns a falsy value or if no function clause matches, the `do` block will not be retried.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: val.starts_with("ok") looks more like snake-cased Scala than Elixir 😛. Do you mean String.starts_with?(val, "ok")?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol. Yeah my scala background is showing. I'll fix it.

@nathanalderson
Copy link
Contributor Author

How are we feeling about this? Anything I can do to push it forward?

@nathanalderson
Copy link
Contributor Author

Periodic bump. Any feedback? Can we merge?

@greenfork
Copy link

Works great, I'm using your branch @nathanalderson , thank you for this patch!

@safwank safwank merged commit 9eeae7d into safwank:master Sep 22, 2024
@nathanalderson
Copy link
Contributor Author

Thanks for merging!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants