sasa1977 / con_cache Goto Github PK
View Code? Open in Web Editor NEWets based key/value cache with row level isolated writes and ttl support
License: MIT License
ets based key/value cache with row level isolated writes and ttl support
License: MIT License
Hi,
If I create a process with ttl
setting and later use ConCache.update/3
to set the value:
ConCache.update(cache, :a, fn(_) -> {:ok, 1} end)
The :a
item never gets expired. Is it correct behaviour? I'll push a failing scenario
I've had the following error happen a few times. At first I assumed it was the server under high load but it's happened under moderate/low load.
** (stop) exited in: GenServer.call(#PID<0.4240.0>, {:lock, 31185, #Reference<0.3051168309.1385955342.117529>}, 5000)
** (EXIT) time out
(elixir 1.14.3) lib/gen_server.ex:1038: GenServer.call/3
(con_cache 1.0.0) lib/con_cache/lock.ex:22: ConCache.Lock.exec/4
(central 0.1.0) lib/teiserver/protocols/spring/spring_out.ex:485: Teiserver.Protocols.SpringOut.do_reply/2
The call that triggers this is ultimately a ConCache.get_or_store
call. It's one of our more commonly called functions (converting a userid to a username) if that helps.
What would be the most appropriate way to deal with this, debug it and the like?
Thoughts on adding configuration to allow LockSupervisor n_partitions to be configurable?
Unfortunately I don't have much more information to try to reproduce, but if ConCache crashes, after the Supervisor restarts it keeps repeating:
** (MatchError) no match of right hand side value: []
(con_cache) lib/con_cache/registry.ex:18: ConCache.Registry.get/1
(con_cache) lib/con_cache.ex:143: ConCache.get/2
(fred_engine) lib/engine/message.ex:5: Fred.Engine.Message.work/1
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
(stdlib) erl_eval.erl:228: :erl_eval.expr/5
(elixir) lib/enum.ex:1628: anonymous fn/3 in Enum.reduce/3
(elixir) lib/range.ex:92: Enumerable.Range.reduce/5
(elixir) lib/enum.ex:1627: Enum.reduce/3
As we discussed in the pull request #26 , I'm opening this issue to discuss about the usage of get_or_store for bag types. I'll prepare a summary of what was discussed in the pr about this subject.
$ mix test
17:31:52.946 [error] Error in process <0.133.0> with exit value: {badarg,[{erlang,send,[nil,{'$gen_cast',{lock,<0.133.0>,2}}],[]},{'Elixir.GenServer',do_send,2,[{file,"lib/gen_server.ex"},{line,424}]},{'Elixir.ConCache.Lock',exec,4,[{file,"lib/con_cache/lock.ex"},{line,24}]}]}
17:31:52.947 [error] Error in process <0.136.0> with exit value: {badarg,[{erlang,send,[nil,{'$gen_cast',{lock,<0.136.0>,2}}],[]},{'Elixir.GenServer',do_send,2,[{file,"lib/gen_server.ex"},{line,424}]},{'Elixir.ConCache.Lock',exec,4,[{file,"lib/con_cache/lock.ex"},{line,24}]}]}
17:31:52.947 [error] Error in process <0.139.0> with exit value: {badarg,[{erlang,send,[nil,{'$gen_cast',{lock,<0.139.0>,2}}],[]},{'Elixir.GenServer',do_send,2,[{file,"lib/gen_server.ex"},{line,424}]},{'Elixir.ConCache.Lock',exec,4,[{file,"lib/con_cache/lock.ex"},{line,24}]}]}
...............................
Finished in 5.2 seconds (0.3s on load, 4.9s on tests)
32 tests, 1 failures
Randomized with seed 937703
I have a few relatively simple caches that look like this:
Supervisor.child_spec(
{ConCache, [name: :active_users_cache, ttl_check_interval: :timer.seconds(10), global_ttl: :timer.minutes(1)]},
id: {ConCache, :active_users_cache}
),
# .... more with exp
Supervisor.child_spec(
{ConCache, [name: :jobs_cache, ttl_check_interval: false]},
id: {ConCache, :jobs_cache}
)
The active_users_cache just contains a user id and a timestamp (their latest request), added like so:
ConCache.put(:active_users_cache, [id], %ConCache.Item{value: DateTime.utc_now()})
But AFAICT, it's never expiring. When I inspect the ets table I see entries like this:
{"250", ~U[2024-02-02 18:08:06.498382Z]},
{"26", ~U[2024-02-02 18:26:50.815029Z]},
{"236", ~U[2024-02-02 18:49:55.533782Z]},
{"279", ~U[2024-02-02 18:49:55.812741Z]},
{"276", ~U[2024-02-02 18:07:00.917742Z]},
{"34", ~U[2024-02-02 18:49:01.212477Z]},
{"275", ~U[2024-02-02 16:48:50.586609Z]},
{"14", ~U[2024-02-02 16:55:30.335637Z]}
My understanding would be with my ttl settings I should never expect to see 2 entries more than 70 secs apart since the put
should overwrite the last value, meaning the timestamp should reflect the last time the ttl was set.
Aside from exp the values all look correct and it works as expected.
Is there anything obvious I am doing wrong? What is the recommended way to go about troubleshooting this?
I would like to perform a get_or_store, check if the value exists in the cache, if not, I would like to call db store lookup function, reload the result of that into the cache and if that return is {:ok, nil} I would like to inform the cache that the value doesn't exist.
My use case is as follows: I want to save a copy of my cache entry to the database so that I can persist that value over my cluster (Riak). If a user checks for a value that is missing in the cache, the system would check if that value exists in the db store, if it does then it loads it into the cache.
On cache delete, I would also like to delete the instance of that key from the data store.
In the readme file the last line contains I don't maintain that project anymore, so I'm not aware of its current status.
although that claim is more than 3 years old. Since then a lot of changes have been done.
I guess it should be removed as it can make people look for other similar projects instead of trying this one.
I think I got this wrong. My understanding was this:
ttl_check: :timer.seconds(15),
To me, that means the duration of the interval it takes to run/rerun the cache cleanup for expired cache
So in my code, I am setting the expiry to about 30days, I wanted this code to run every say 15 seconds and ONLY delete the expired cache. It is not working that way, what it's doing is deleting all my caches irrespective of their expiry time.
What was the intended use case for this and how can I achieve my original goal?
1.5 was just released.
The get_or_store function can not be properly used due to the fact that it returns different values whether it gets or stores.
If I'm missing the intended use case, please provide an example. Otherwise the correct implementation should be to always return the value, either after get, either after storing (instead of :ok).
First off, thanks for the great library. It's working exactly as advertised.
One issue I'm running into is how to manage ConCache
in my tests. I was having order-dependent failures until I realized that the cached values were persisting between tests. I looked for a ConCache.flush
(or similar) function but could not fine one. For now I am manually calling ConCache.delete
for each key that has been cached. This is pretty cumbersome and brittle so a function to simplify this would be much appreciated.
Trying to follow #18 for persistent caching.
It seems that trying to save the cache to a tab using :file2tab works but restoring the cache doesn't
ConCache.put(:table_name, :test, "hello")
:table_name
|> ConCache.ets
|> :ets.tab2file('file-path.tab')
# restart and inside start_link
{:ok, ref} = :ets.file2tab('file-path.tab')
# reference returned by this is wrong and not the
# same as `ConCache.ets(:table_name)`
:ets.tab2list(ref) # [test: "hello"]
ConCache.get(:table_name, :test) # nil
Thank you.
If example with phoenix, many thanks.
It would be nice to prevent touching the TTL when updating. I'm using con_cache as a rate limit store, so it should simply expire after a duration after the key is first created. Updates wouldn't affect the ttl.
Looking for something like:
{limit, duration} = {15, :min}
used = ConCache.get_or_store(:ratelimit, key, fn ->
%ConCache.Item{value: 0, ttl: ttl(limit, duration)}
end)
if used >= limit do
conn = conn |> Plug.Conn.send_resp(429, "Too many requests") |> Plug.Conn.halt
else
ConCache.update(:tally, key, fn old_value ->
{:ok, %ConCache.Item{value: min(old_value + 1, limit), ttl: :keep}
end)
used = ConCache.get(:tally, key)
conn = conn |> Plug.Conn.merge_resp_headers(%{"x-ratelimit-remaining" => to_string(max(limit - used, 0))})
end
I think it's a bit non-intuitive to not have a default TTL Check.
If a default one is unwieldy (as it should be based on an application's requirements) I think a more prominent warning on the console on development mode would be great
I've tried to start a :bag
table, but when I retrieved the data only returned the last value inserted.
There is any plan on support other type of tables then :set
?
I am using ConCahe for saving an image with its modified timestamps. It works fine, normally, and when I say normally then it works fine in logs as
def update_cache_and_save_thumbnail(camera_exid, timestamp, image) do
{last_save_date, t, img} = ConCache.dirty_get_or_store(:camera_thumbnail, camera_exid, fn() ->
{Calendar.DateTime.now!("UTC"), timestamp, image}
end)
Logger.info "This is dirty get or store"
IO.inspect ConCache.get(:camera_thumbnail, camera_exid)
Logger.info "==========================="
case Calendar.DateTime.diff(Calendar.DateTime.now!("UTC"), last_save_date) do
{:ok, seconds, _, :after} ->
thumbnail_save_seaweedfs(camera_exid, image, timestamp, last_save_date, seconds)
Logger.info "This is after thumnail save_seaweedfs"
IO.inspect ConCache.get(:camera_thumbnail, camera_exid)
Logger.info "==========================="
_ ->
Logger.info "This is will never run i think"
IO.inspect ConCache.get(:camera_thumbnail, camera_exid)
Logger.info "==========================="
ConCache.dirty_put(:camera_thumbnail, camera_exid, {last_save_date, timestamp, image})
end
end
when this function run and I am calling ConCache.get(:camera_thumbnail, "blessington_court")
in between and it's giving me an updated value right away. such as
[info] This is dirty get or store
{#DateTime<2018-10-09 06:58:37.474918+00:00 UTC UTC>, 1539067937,
<<255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 2, 0, 0, 1, 0, 1, 0, 0, 255,
254, 0, 15, 10, 1, 91, 188, 80, 34, 11, 91, 188, 80, 34, 11, 1, 255, 254, 0,
15, 10, 0, 1, 219, 5, 80, ...>>}
[info] ===========================
{#DateTime<2018-10-09 06:58:37.474918+00:00 UTC UTC>, 1539067919,
<<255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 2, 0, 0, 1, 0, 1, 0, 0, 255,
254, 0, 15, 10, 1, 91, 188, 80, 21, 58, 91, 188, 80, 21, 58, 1, 255, 254, 0,
15, 10, 0, 1, 219, 5, 80, ...>>}
[info] This is after thumnail save_seaweedfs
[info] ===========================
[info] This is dirty get or store
{#DateTime<2018-10-09 06:58:37.474918+00:00 UTC UTC>, 1539067919,
<<255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 2, 0, 0, 1, 0, 1, 0, 0, 255,
254, 0, 15, 10, 1, 91, 188, 80, 21, 58, 91, 188, 80, 21, 58, 1, 255, 254, 0,
15, 10, 0, 1, 219, 5, 80, ...>>}
[info] ===========================
{#DateTime<2018-10-09 06:58:37.474918+00:00 UTC UTC>, 1539067934,
<<255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 2, 0, 0, 1, 0, 1, 0, 0, 255,
254, 0, 15, 10, 1, 91, 188, 80, 31, 78, 91, 188, 80, 31, 78, 1, 255, 254, 0,
15, 10, 0, 1, 219, 5, 80, ...>>}
[info] This is after thumnail save_seaweedfs
[info] ==========================
for once it gives duplicate but that's because it's been looking in cache. but whenever I do in the remote console as ConCache.get(:camera_thumbnail, "blessington_court"), It always gives the very old value. not the updated one.
{#DateTime<2018-10-09 06:25:56.691951+00:00 UTC UTC>, 1539065928,
<<255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 2, 0, 0, 1, 0, 1, 0, 0, 255,
254, 0, 15, 10, 1, 91, 188, 72, 63, 84, 91, 188, 72, 63, 84, 1, 255, 254, 0,
15, 10, 0, 1, 219, 5, 80, ...>>}
I have read the module docs for concache, But I didn't find anything suitable which can pas the theory that it will give new value right away but old one when I can method by myself.? Can you guide me what is wrong happening in this? I tried this several times but it's giving me an old value always.
Is there any way to persist the ETS table across a restart/crash? Thanks in advance!
Recently, I noticed that cache items don't expire anymore. Results from a new phoenix v1.3.2 app:
iex(2)> {:ok, cache} = ConCache.start_link(ttl_check_interval: :timer.seconds(1), global
_ttl: :timer.seconds(5))
{:ok, #PID<0.403.0>}
iex(3)> ConCache.put(cache, :foo, :bar)
:ok
iex(4)> ConCache.get(cache, :foo)
:bar
iex(5)> :timer.sleep(:timer.seconds(10))
:ok
iex(6)> ConCache.get(cache, :foo)
:bar
iex(7)>
Am I missing something? Thanks.
Hello,
I'm a Elixir/OTP very beginner and I'm struggling with workers and supervisors. Even if I'm learning how to use OTP correctly, is there any easy/faster way to create global caches ? Here is my thought translated to code :
defmodule MyApp.Cache do
use ConCache
end
defmodule MyApp.Supervisor do
use Supervisor
def start_link do
:supervisor.start_link(__MODULE__, [])
end
def init([]) do
children = [
worker(MyApp.Cache, []),
]
supervise(children, strategy: :one_for_one)
end
end
MyApp.Supervisor.start_link
value = MyApp.Cache.get :key
Alpine Linux 3.7
(using Docker)Elixir 1.5.3 (compiled with OTP 20)
ConCache 0.12.1
Currently attempting to integrate ConCache into my existing project, which uses a module-based supervisor to manage child processes:
defmodule MyService.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, :ok, opts)
end
def init(:ok) do
children = [
...
{ConCache, [name: :cache, ets_options: [:set]]}, id: :cache_1, type: :supervisor)
]
Supervisor.init(children, strategy: :one_for_one)
end
end
Then, I attempt to call my cache from one of the other children processes in the above Supervision tree. However, my application is crashing with the following error:
The module ConCache was given as a child to a supervisor
but it does not implement child_spec/1.
...
However, if you don't own the given module and it doesn't implement
child_spec/1, instead of passing the module name directly as a supervisor
child, you will have to pass a child specification as a map:
%{
id: ConCache,
start: {ConCache, :start_link, [arg1, arg2]}
}
See the Supervisor documentation for more information.
So, I replaced my Supervisor.child_spec/2
function as follows:
...
children = [
...
%{
id: :cache_1,
start: {ConCache, :start_link, [[name: :cache, ets_options: [:set]]]},
type: :supervisor
}
]
This causes my application to crash with the following error:
[error] Task #PID<0.231.0> started from #PID<0.222.0> terminating
** (stop) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
(con_cache) lib/con_cache/owner.ex:17: ConCache.Owner.cache/1
(con_cache) lib/con_cache.ex:149: ConCache.put/3
Is there an existing workaround to get this functioning?
Hi,
I am using con_cache
version 0.10.0
When I run these commands I face an error :
iex(1)> options = [ttl_check: :timer.seconds(1),ttl: :timer.seconds(1)]
[ttl_check: 1000, ttl: 1000]
iex(2)> gen_server_options = []
[]
iex(3)> {:ok, cache} = ConCache.start_link(options, gen_server_options)
{:ok, #PID<0.315.0>}
iex(4)> ConCache.update(cache, :a, fn(_old) -> {:ok, %ConCache.Item{value: 1, ttl: :timer.seconds(10)}} end)
:ok
iex(5)> ConCache.get(cache, :a)
1
iex(6)> ConCache.update(cache, :a, fn(value) -> {:ok, %ConCache.Item{value: value + 1, ttl: :no_update}} end)
:ok
iex(7)> ConCache.get(cache, :a)
2
** (EXIT from #PID<0.311.0>) an exception was raised:
** (FunctionClauseError) no function clause matching in ConCache.Owner.store_ttl/3
(con_cache) lib/con_cache/owner.ex:153: ConCache.Owner.store_ttl(%ConCache.Owner{current_time: 1, max_time: 65535, monitor_ref: #Reference<0.0.1.237>, on_expire: #Function<0.74405522/1 in ConCache.Owner.start_ttl_loop/1>, pending: 159802, pending_ttl_sets: #HashDict<[a: :no_update]>, ttl_check: 1000, ttls: 163899}, :a, :no_update)
(elixir) lib/enum.ex:610: anonymous fn/3 in Enum.each/2
(elixir) lib/enum.ex:1478: anonymous fn/3 in Enum.reduce/3
(elixir) lib/hash_dict.ex:188: HashDict.do_reduce_each/4
(elixir) lib/enum.ex:1477: Enum.reduce/3
(elixir) lib/enum.ex:609: Enum.each/2
(con_cache) lib/con_cache/owner.ex:113: ConCache.Owner.apply_pending_ttls/1
(con_cache) lib/con_cache/owner.ex:182: ConCache.Owner.handle_info/2
(stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:681: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
iex(8)> [error] GenServer #PID<0.315.0> terminating
** (FunctionClauseError) no function clause matching in ConCache.Owner.store_ttl/3
(con_cache) lib/con_cache/owner.ex:153: ConCache.Owner.store_ttl(%ConCache.Owner{current_time: 1, max_time: 65535, monitor_ref: #Reference<0.0.1.237>, on_expire: #Function<0.74405522/1 in ConCache.Owner.start_ttl_loop/1>, pending: 159802, pending_ttl_sets: #HashDict<[a: :no_update]>, ttl_check: 1000, ttls: 163899}, :a, :no_update)
(elixir) lib/enum.ex:610: anonymous fn/3 in Enum.each/2
(elixir) lib/enum.ex:1478: anonymous fn/3 in Enum.reduce/3
(elixir) lib/hash_dict.ex:188: HashDict.do_reduce_each/4
(elixir) lib/enum.ex:1477: Enum.reduce/3
(elixir) lib/enum.ex:609: Enum.each/2
(con_cache) lib/con_cache/owner.ex:113: ConCache.Owner.apply_pending_ttls/1
(con_cache) lib/con_cache/owner.ex:182: ConCache.Owner.handle_info/2
(stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:681: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: :check_purge
State: %ConCache.Owner{current_time: 1, max_time: 65535, monitor_ref: #Reference<0.0.1.237>, on_expire: #Function<0.74405522/1 in ConCache.Owner.start_ttl_loop/1>, pending: 159802, pending_ttl_sets: #HashDict<[a: :no_update]>, ttl_check: 1000, ttls: 163899}
Something strange is going on, I wanted to reproduce the same problem in con_cache
tests but the same code works on the test
diff --git a/test/con_cache_test.exs b/test/con_cache_test.exs
index 9f6f60a..c6113f8 100644
--- a/test/con_cache_test.exs
+++ b/test/con_cache_test.exs
@@ -200,6 +200,16 @@ defmodule ConCacheTest do
end)
end
+ test "first update with ttl and second update without touching ttl" do
+ with_cache(
+ [ttl_check: :timer.seconds(1),ttl: :timer.seconds(1)],
+ fn(cache) ->
+ ConCache.update(cache, :a, fn(_old) -> {:ok, %ConCache.Item{value: 1, ttl: :timer.seconds(1)}} end)
+ ConCache.update(cache, :a, fn(value) -> {:ok, %ConCache.Item{value: value + 1, ttl: :no_update}} end)
+ assert ConCache.get(cache, :a) == 2
+ end)
+ end
+
defp test_renew_ttl(cache, fun) do
ConCache.put(cache, :a, 1)
:timer.sleep(50)
mix test
....................................
Finished in 5.4 seconds (0.2s on load, 5.2s on tests)
36 tests, 0 failures
Randomized with seed 698014
Maybe I missed it, but I couldn't find a reference to a license for this code. Is it meant to be in the public domain?
If you're looking for a license, I've created one, based on my particular preferences, which you're welcome to use... https://github.com/nirvana/TFL
I can't find any benchmark result for performance,is there a report doc about this?
Thanks.
I started a cache with
supervisor(ConCache, [[], [name: :queue]])
And then, by mistake, called ConCache.put(:my_cache, :key, :value)
(notice the wrong cache name). The call just hangs. Wouldn't it be better for it to throw an exception instead?
Elixir: 1.4.4
con_cache: 0.12.0
Hello, is there a way to send an event when any row expires?
I'm now trying to use Concache to replace redis in the project, it works pretty well. But one feature I'm missing here is redis.keys pattern.
Basically since all keys in redis is binary based, for erlang I'm planning to use a tuple key like
{name,age,index} = value and put into ConCache.
If I extend it using the ets match/select query I think this can be done for some pattern matches with the key, but I don't know the impact if will have to the performance, or if any drawbacks it might have in regards to TTL mechanism.
Any suggestions will be really helpful, thx for your efforts to create this project :)
Hey, thanks for this library.
Would you be interested in an extension allowing querying each cache's hit rate? Happy to discuss implementation details if you're open to the idea.
Hello, is there a way to call the callback function after delete operation is done instead of before?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.