-
Notifications
You must be signed in to change notification settings - Fork 75
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
Support for functions that can set TTL in Decorator similar to Match #200
Comments
This is a nice suggestion/feature, and doesn't seem hard to implement, will give it a priority to have it available soon. Thanks! |
Hey 👋 ! I pushed a solution for this, but I changed the proposal a bit, like this:
The idea was to be able to define the storing options at runtime, not just the @decorate cacheable(cache: Cache,
key: {ExternalApi.Cert, :get_cert},
match: &cert_legit/1)
# returns %{:ok, %{exp: time_to_expiry_of_cert}}
defp get_cert(), do: ExternalApi.Cert.get_Cert()
defp cert_legit({:ok, %{exp: exp}} = ok), do: {true, ok, [ttl: exp]}
defp cert_legit(_), do: false Please let me know if that works for you, thanks! |
Closing this issue, but feel free to reopen it if you come across issues. |
Hey Carlos, This is clearly a road to a much more elegant and extensible way to solve the problem, was planning to play around with it in the weekend and couldn't get back to you earlier. Since the cache value is now dynamic should the first return before the cache key is formed also follow that transformation Currently the subsequent value returns as per that transformation to which its stored to the cache. Here is an example of such a process defmodule CacheTest do
use Nebulex.Caching
@decorate cacheable(cache: Phos.Cache,
key: {CacheTest, :get_cert},
match: &time_legit/1)
# returns %{:ok, %{exp: time_to_expiry_of_cert}}
def get_time() do
host = "http://127.0.0.1:4040"
with {:ok, %{body: body}} <- HTTPoison.get(host <> "/ping",[{"Content-Type","application/json"}]) do
{:ok, %{body: body, exp: 10000}}
end
end
defp time_legit({:ok, %{exp: exp, body: body}} = ok), do: {true, body, [ttl: exp]}
defp time_legit(_), do: false
end Upon calling Maybe this behavior can be explicitly toggled on or off in case current behavior is preferred |
Right, when returning the tuple @decorate cacheable(cache: Phos.Cache,
key: {CacheTest, :get_cert},
match: &time_legit/1)
# returns %{:ok, %{exp: time_to_expiry_of_cert}}
def get_time() do
host = "http://127.0.0.1:4040"
with {:ok, %{body: body}} <- HTTPoison.get(host <> "/ping",[{"Content-Type","application/json"}]) do
{:ok, %{body: body, exp: 10000}}
end
end
defp time_legit({:ok, %{exp: exp, body: body}} = ok), do: {true, ok, [ttl: exp]}
defp time_legit(_), do: false I'm not sure if that is what you meant, please let me know, stay tuned, thanks !!! |
That is indeed the best way to use the currently exposed functionality for the subsequent calls to cache to follow the shape of the data returned by your own function, but in terms of ergonomics of the API if someone wishes to transform the data before storing in cache differently this does force one to create wrapper function to standardise the shape of the data. As an example if one does not wish you have the exp key to be exposed to functions calling CacheTest.get_time() you will have a helper function dropping the :exp key. Since quote do
result = unquote(block)
unquote(__MODULE__).run_cmd(
unquote(__MODULE__),
:eval_match,
[result, match, cache, unquote(key), opts],
on_error,
result
)
result
Might be an abuse of match to expose match.(result)'s value here to override result |
What you have described is totally correct. However, if you have a proposal or idea about how we can improve it, please let me know, it would be great to discuss other alternatives and/or ideas. And BTW, thanks a lot for the TTL proposal, I think this has been a nice feature 👍 |
Hey Cabol, I have a simple proof of concept that might be to your liking that I implemented for the @Cacheable decorator that returns the desired cached value for the initial call as well. I broke it into two commits and another that adds a :return_cached flag that keeps to the current default behavior that you can flip if you feel the alternative is saner A sample program to test the behavior Application.put_env(:test, Test.Cache, gc_interval: 86_400_000)
defmodule Test.Cache do
use Nebulex.Cache,
otp_app: :test,
adapter: Nebulex.Adapters.Local
end
defmodule Test.Grain do
use Nebulex.Caching
@decorate cacheable(
cache: Test.Cache,
key: {Grain, :get},
match: &grain_legit/1
)
# returns weight of grain everytime in integer
def get() do
with {:ok, %{grain: weight}} <- {:ok, %{grain: 10000}} do
{:ok, %{grain: weight}}
end
end
defp grain_legit({:ok, %{grain: weight}}), do: {true, weight, [ttl: floor(weight/10), return_cached: true]}
defp grain_legit(_), do: false
end If you feel this is up to standard I can extend the behaviour for the other decorators that utilize eval_match() as well |
Hey @KosmonautX 👋 !! Apologies for the lateness here, I've been quite busy with other stuff, including Nebulex v3, but let's get into it. IMHO, I don't see in what scenarios the defmodule Test.Grain do
use Nebulex.Caching
@decorate cacheable(
cache: Test.Cache,
key: {Grain, :get},
match: &grain_legit/1
)
# returns weight of grain everytime in integer
def get() do
with {:ok, %{grain: weight}} <- {:ok, %{grain: 10000}} do
weight
end
end
# You can match the success like this, and otherwise return the false
defp grain_legit(weight) when is_integer(weight), do: {true, weight, [ttl: floor(weight/10)]}
defp grain_legit(_), do: false
# You could also match the error case(s) first and ensure returning false, and otherwise the true
# defp grain_legit({:error, _}), do: false
# defp grain_legit(weight), do: {true, weight, [ttl: floor(weight/10)]}
end In this case, the @decorate cacheable(key: id)
def get_book(id) do
# This returns the Book schema from the DB or nil
Repo.get(Book, id)
end
@decorate cache_put(key: book.id, match: &match_fun/1)
def update_book(%Book{} = book, attrs) do
# On the other hand this returns an Ok/Error tuple, so we need the match
# to cached only the value to be consistent with the cacheable decorator
book
|> Book.changeset(attrs)
|> Repo.update()
end
defp match_fun({:ok, usr}), do: {true, usr}
defp match_fun({:error, _}), do: false But overall, if you can write your code in such a way you don't need to worry about the @decorate cacheable(key: id)
def fetch_book(id) do
# Wrapper for returning the Ok/Error tuple
if book = Repo.get(Book, id) do
# This is the cached result which is consistent with the cache_put
{:ok, book}
else
{:error, :not_found}
end
end
@decorate cache_put(key: book.id)
def update_book(%Book{} = book, attrs) do
# Returns an Ok/Error tuple
book
|> Book.changeset(attrs)
|> Repo.update()
end Anywho, please let me know your thoughts, if I'm missing something, etc. Stay tuned, thanks 🙇 !!! |
Proposed Behaviour
Suppose we have a certificate that expires in
time_to_expiry_of_cert
secondsThis acts similarly to the current that decides whether the code-block evaluation result is cached or not
:match - Match function (term -> boolean | {true, term}) (optional). This function is for matching and decide whether the code-block evaluation result is cached or not. If true the code-block evaluation result is cached as it is (the default). If {true, value} is returned, then the value is what is cached (useful to control what is meant to be cached). Returning false will cause that nothing is stored in the cache.
Current Behaviour
The text was updated successfully, but these errors were encountered: