whitfin / cachex

A powerful caching library for Elixir with support for transactions, fallbacks and expirations
https://hexdocs.pm/cachex/
MIT License
1.6k stars 103 forks source link

Cachex warmer breaks the application with "no match of right hand side value: {:error, :no_cache}" error #323

Closed vasumur closed 9 months ago

vasumur commented 9 months ago

Using Cachex 3.6 version and here is the runtime environment details for the application

erlang 26.0.2
elixir 1.15.6-otp-26
whitfin commented 9 months ago

Do you have a reproduction case? I can't do much here without.

vasumur commented 9 months ago

I can put together one, but this seems to be the same as https://github.com/whitfin/cachex/issues/318, which is closed.

In that case should i be waiting for a new version of Cachex etc to use?

whitfin commented 9 months ago

318 is a test only issue (inside the Cachex tests themselves), so I don't think this is the same problem assuming you're seeing it in your own application?

There is a pending Cachex release in the next few weeks, so I have some time to fix anything here if it needs it!

vasumur commented 9 months ago

@whitfin Appreciate you help in looking in this.

I am having trouble putting together the exact condition that raises this error. In the application many of these key refreshes are running database queries etc. I suspect something to do with the timing and/or order in which the caches are created.

The scenario as in the code is here in this sample code

https://github.com/vasumur/cachex_warmer_test

The error happens while putting the key into :cache1 (that is the scenario in my app), it says that cache1 is not available yet thought that was the first one in the list of caches.

whitfin commented 9 months ago

@vasumur I've glanced through that project, I can sit down more later if needed.

I'm not sure if this is something else; you mentioned that the issue is with :cache1 but there are no warmers registered with that cache. Are you sure it's not that your web app is trying to access that cache before it's created? If this is the case, you need to place your caches earlier in the Supervision tree than whatever it is that's calling :cache1 (I noticed they're at the end).

vasumur commented 9 months ago

Yes it is the cache1 that gets the no cache error. And the place where it gets the error is in that "initialize_app" in application.ex file.

Apologies for getting back late.

vasumur commented 9 months ago

@whitfin

I was able to simulate this and the changes are posted in the git repository

https://github.com/vasumur/cachex_warmer_test

If you run the code with the below MIX_ENV value, you will get the error I was referring to.

MIX_ENV=vn_dev iex -S mix phx.server

Where as running with the below command (even skipping the MIX_ENV as well) works fine.

MIX_ENV=dev iex -S mix phx.server

The Erland and Elixir versions I used are as below (and also in the .tool-versions file in the repository).

erlang 26.0.2 elixir 1.15.2-otp-26

whitfin commented 9 months ago

Hi @vasumur!

I took a look at your example:

** (Mix) Could not start application cachex_warmer_test: CachexWarmerTest.Application.start(:normal, []) returned an error: shutdown: failed to start child: CachexWarmerTestWeb.Endpoint
    ** (EXIT) an exception was raised:
        ** (UndefinedFunctionError) function Phoenix.LiveReloader.Socket.child_spec/1 is undefined (module Phoenix.LiveReloader.Socket is not available)
            Phoenix.LiveReloader.Socket.child_spec([endpoint: CachexWarmerTestWeb.Endpoint])
            (phoenix 1.7.10) lib/phoenix/endpoint/supervisor.ex:117: anonymous fn/4 in Phoenix.Endpoint.Supervisor.socket_children/2
            (elixir 1.14.5) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
            (phoenix 1.7.10) lib/phoenix/endpoint/supervisor.ex:116: Phoenix.Endpoint.Supervisor.socket_children/2
            (phoenix 1.7.10) lib/phoenix/endpoint/supervisor.ex:81: Phoenix.Endpoint.Supervisor.init/1
            (stdlib 5.1) supervisor.erl:330: :supervisor.init/1
            (stdlib 5.1) gen_server.erl:962: :gen_server.init_it/2
            (stdlib 5.1) gen_server.erl:917: :gen_server.init_it/6
            (stdlib 5.1) proc_lib.erl:241: :proc_lib.init_p_do_apply/3

This happens if I comment out the called to initialize_app/1, however this crash is being masked by the assertion crashing after calling Cachex:

{:ok, true} = Cachex.put(:cache1, "some", "value")

The cache is being terminated because your application tree is failing to start properly, so when you try call it it's unavailable (which is why you get :no_cache). So I think maybe this is a misdirected error and there's nothing wrong with your cache?

Of course I know this is just a sample project so if it's not the case in your real use case I'm happy to look further!

vasumur commented 9 months ago

Hi @vasumur!

I took a look at your example:

** (Mix) Could not start application cachex_warmer_test: CachexWarmerTest.Application.start(:normal, []) returned an error: shutdown: failed to start child: CachexWarmerTestWeb.Endpoint
    ** (EXIT) an exception was raised:
        ** (UndefinedFunctionError) function Phoenix.LiveReloader.Socket.child_spec/1 is undefined (module Phoenix.LiveReloader.Socket is not available)
            Phoenix.LiveReloader.Socket.child_spec([endpoint: CachexWarmerTestWeb.Endpoint])
            (phoenix 1.7.10) lib/phoenix/endpoint/supervisor.ex:117: anonymous fn/4 in Phoenix.Endpoint.Supervisor.socket_children/2
            (elixir 1.14.5) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
            (phoenix 1.7.10) lib/phoenix/endpoint/supervisor.ex:116: Phoenix.Endpoint.Supervisor.socket_children/2
            (phoenix 1.7.10) lib/phoenix/endpoint/supervisor.ex:81: Phoenix.Endpoint.Supervisor.init/1
            (stdlib 5.1) supervisor.erl:330: :supervisor.init/1
            (stdlib 5.1) gen_server.erl:962: :gen_server.init_it/2
            (stdlib 5.1) gen_server.erl:917: :gen_server.init_it/6
            (stdlib 5.1) proc_lib.erl:241: :proc_lib.init_p_do_apply/3

This happens if I comment out the called to initialize_app/1, however this crash is being masked by the assertion crashing after calling Cachex:

{:ok, true} = Cachex.put(:cache1, "some", "value")

The cache is being terminated because your application tree is failing to start properly, so when you try call it it's unavailable (which is why you get :no_cache). So I think maybe this is a misdirected error and there's nothing wrong with your cache?

Of course I know this is just a sample project so if it's not the case in your real use case I'm happy to look further!

My mistake, I was too happy to see that no_cache error and submitted this. Should have checked further.

Regardless the issue with the warmers are present in my application. Still trying to find out why the :cache1 (though it doesn't have warmers) is still not initialized.

When the below statement in Application.ex returns, can i assume that all the caches (which are in the children list) are initialized?

    ret = Supervisor.start_link(children, opts)
whitfin commented 9 months ago

I'm a little confused by this issue. If :cache1 has no warmers, then what is allowing you to say that warmers are the thing breaking the cache?

Are you adding warmers to some other cache? Are you sure your warmers actually function correctly and aren't crashing on startup (thus stopping your supervision tree)?

The fact that :cache1 is not starting means that your supervision tree is being unwound, the same as your sample project - I can't really say why without seeing it. If you're saying that adding a warmer to another cache is causing it, then it's almost guaranteed to be some crash in your warming code (because it runs for the first time on cache startup). There's simply no case that :cache1 would randomly die because you added a cache warmer to another cache; there's zero interaction between them other than the supervision tree.

When the below statement in Application.ex returns, can i assume that all the caches (which are in the children list) are initialized?

Yes, linking a cache does not return until all tables are initialized, all services are initialized, all hooks started, all necessary warmers are run, etc.

vasumur commented 9 months ago

Thanks @whitfin. I found the issue with the warmer where at times it is not providing the list of tuples and fixed it. That corrected the problem. Appreciate your help with this.

I got distracted with the error on a cache which is not even related to warmer. Will close this ticket