{"id":13507522,"url":"https://github.com/sasa1977/con_cache","last_synced_at":"2025-05-13T19:18:05.866Z","repository":{"id":7730822,"uuid":"9097144","full_name":"sasa1977/con_cache","owner":"sasa1977","description":"ets based key/value cache with row level isolated writes and ttl support","archived":false,"fork":false,"pushed_at":"2025-01-30T23:00:20.000Z","size":347,"stargazers_count":924,"open_issues_count":4,"forks_count":73,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-05-13T12:54:32.831Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sasa1977.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2013-03-29T11:21:29.000Z","updated_at":"2025-05-04T20:49:53.000Z","dependencies_parsed_at":"2023-10-03T10:49:32.150Z","dependency_job_id":"955721b6-273e-4649-b6ae-dc6c6c5a2acf","html_url":"https://github.com/sasa1977/con_cache","commit_stats":{"total_commits":231,"total_committers":28,"mean_commits":8.25,"dds":0.3073593073593074,"last_synced_commit":"fdf1d1c82268fffefaf8ea2f3057c0ba9c6f0b46"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa1977%2Fcon_cache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa1977%2Fcon_cache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa1977%2Fcon_cache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa1977%2Fcon_cache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sasa1977","download_url":"https://codeload.github.com/sasa1977/con_cache/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254010830,"owners_count":21999004,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T02:00:35.636Z","updated_at":"2025-05-13T19:18:05.822Z","avatar_url":"https://github.com/sasa1977.png","language":"Elixir","funding_links":[],"categories":["Caching"],"sub_categories":[],"readme":"# ConCache\n\n![Build Status](https://github.com/sasa1977/con_cache/workflows/con_cache/badge.svg?branch=master)\n[![Module Version](https://img.shields.io/hexpm/v/con_cache.svg)](https://hex.pm/packages/con_cache)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/con_cache/)\n[![Total Download](https://img.shields.io/hexpm/dt/con_cache.svg)](https://hex.pm/packages/con_cache)\n[![License](https://img.shields.io/hexpm/l/con_cache.svg)](https://github.com/sasa1977/con_cache/blob/master/LICENSE)\n[![Last Updated](https://img.shields.io/github/last-commit/sasa1977/con_cache.svg)](https://github.com/sasa1977/con_cache/commits/master)\n\nConCache (Concurrent Cache) is an ETS based key/value storage with following additional features:\n\n- row level synchronized writes (inserts, read/modify/write updates, deletes)\n- TTL support\n- modification callbacks\n\n## Usage in OTP applications\n\nAdd `con_cache` as a dependency to your `mix.exs`:\n\n```elixir\n\n  defp deps do\n    [\n      {:con_cache, \"~\u003e 1.0\"},\n      ...\n    ]\n  end\n```\n\nA cache can be started using `ConCache.start` or `ConCache.start_link` functions. Both functions take two arguments - the first one being a list of ConCache options, and the second one a list of GenServer options for the process being started.\n\nTypically you want to start the cache from a supervisor:\n\n```elixir\nchildren = [\n  {ConCache, [name: :my_cache, ttl_check_interval: false]}\n  ...\n]\n\nSupervisor.start_link(children, options)\n```\n\nFor OTP apps, you can generally find this in `lib/\u003cmyapp\u003e.ex`. In the Phoenix web framework, look in the `start` function and add the worker to the `children` list.\n\nNotice the `name: :my_cache` option. The resulting process will be registered under this alias. Now you can use the cache as follows:\n\n```elixir\n# Note: all of these requests run in the caller process, without going through\n# the started process.\n\nConCache.put(:my_cache, :key, \"value\")         # inserts value or overwrites the old one\nConCache.insert_new(:my_cache, :key, \"value\")  # inserts value or returns {:error, :already_exists}\nConCache.get(:my_cache, :key)\nConCache.delete(:my_cache, :key)\nConCache.size(:my_cache)\n\nConCache.update(:my_cache, :key, fn(old_value) -\u003e\n  # This function is isolated on a row level. Modifications such as update, put, delete,\n  # on this key will wait for this function to finish.\n  # Modifications on other items are not affected.\n  # Reads are always dirty.\n\n  {:ok, \"new_value\"}\nend)\n\n# Similar to update, but executes provided function only if item exists.\n# Otherwise returns {:error, :not_existing}\nConCache.update_existing(:my_cache, :key, fn(old_value) -\u003e\n  {:ok, \"new_value\"}\nend)\n\n# Returns existing value, or calls function and stores the result.\n# If many processes simultaneously invoke this function for the same key, the function will be\n# executed only once, with all others reading the value from cache.\nConCache.get_or_store(:my_cache, :key, fn() -\u003e\n  \"initial_value\"\nend)\n\n# Similar to get_or_store/3 but works with :ok/:error tuples.\n# The value is cached only if the function returns an :ok tuple.\nConCache.fetch_or_store(:my_cache, :key, fn -\u003e\n  case call_api() do\n    # The processed value will be cached and returned as an :ok tuple.\n    {:ok, data} -\u003e {:ok, process_data(data)}\n    # The error tuple is propagated to the caller.\n    {:error, _reason} = error -\u003e error\n  end\nend)\n```\n\nDirty modifiers operate directly on ETS record without trying to acquire the row lock:\n\n```elixir\nConCache.dirty_put(:my_cache, :key, \"value\")\nConCache.dirty_insert_new(:my_cache, :key, \"value\")\nConCache.dirty_delete(:my_cache, :key)\nConCache.dirty_update(:my_cache, :key, fn(old_value) -\u003e ... end)\nConCache.dirty_update_existing(:my_cache, :key, fn(old_value) -\u003e ... end)\nConCache.dirty_get_or_store(:my_cache, :key, fn() -\u003e ... end)\nConCache.dirty_fetch_or_store(:my_cache, :key, fn() -\u003e ... end)\n```\n\n### Callback\n\nYou can register your own function which will be invoked after an element is stored or deleted:\n\n```elixir\n{ConCache, [name: :my_cache, callback: fn(data) -\u003e ... end]}\n\nConCache.put(:my_cache, :key, \"value\")         # fun will be called with {:update, cache_pid, key, value}\nConCache.delete(:my_cache, :key)             # fun will be called with {:delete, cache_pid, key}\n```\n\nThe delete callback is invoked before the item is deleted, so you still have the chance to fetch the value from the cache and do something with it.\n\n### TTL\n\n```elixir\n{ConCache, [\n  name: :my_cache,\n  ttl_check_interval: :timer.seconds(1),\n  global_ttl: :timer.seconds(5)\n]}\n```\n\nThis example sets up item expiry check every second, and sets the global expiry for all cache items to 5 seconds. Since ttl_check_interval is 1 second, the item lifetime might be at most 6 seconds.\n\nHowever, the item lifetime is renewed on every modification. Reads don't extend global_ttl, but this can be changed when starting cache:\n\n```elixir\n{ConCache, [\n  name: :my_cache,\n  ttl_check_interval: :timer.seconds(1),\n  global_ttl: :timer.seconds(5),\n  touch_on_read: true\n]}\n```\n\nIn addition, you can manually renew item's ttl:\n\n```elixir\nConCache.touch(:my_cache, :key)\n```\n\nIf you would like to set a custom ttl for specific key, you can pass a `Concache.Item` struct instead of a raw value:\n\n```elixir\nConCache.put(:my_cache, :key, %ConCache.Item{value: \"value\", ttl: :timer.seconds(25)})\n\nConCache.update(:my_cache, :key, fn(old_value) -\u003e\n  {:ok, %ConCache.Item{value: \"new_value\", ttl: :timer.seconds(25)}}\nend)\n```\n\nAnd you can update an item without resetting the item's ttl:\n\n```elixir\nConCache.put(:my_cache, :key, %ConCache.Item{value: \"value\", ttl: :no_update})\n\nConCache.update(:my_cache, :key, fn(old_value) -\u003e\n  {:ok, %ConCache.Item{value: \"new_value\", ttl: :no_update}}\nend)\n```\n\nIf you use ttl value of `:infinity` the item never expires.\n\nTTL check **is not** based on brute force table scan, and should work reasonably fast assuming the check interval is not too small. I broadly recommend `ttl_check_interval` to be at least 1 second, possibly more, depending on the cache size and desired ttl.\n\nIf needed, you may also pass false to `ttl_check_interval`. This effectively stops `con_cache` from checking the ttl of your items:\n\n```elixir\n{ConCache, [\n  name: :my_cache,\n  ttl_check_interval: false\n]}\n```\n\n### Telemetry\n\nAs of 1.1.0, ConCache emits [telemetry](https://github.com/beam-telemetry/telemetry) events. This allows the user to instrument their application to collect metrics about cache utilization.\n\nCurrently, ConCache emits the following events:\n\n - `[:con_cache, :stats, :hit]` - when cache key lookup succeeds\n - `[:con_cache, :stats, :miss]` - when cache key is not found\n\nEach event comes with `%ConCache{}` struct within its metadata.\n\nExample handler:\n\n```elixir\ndefmodule MyApp.HitMissRatioTracker do\n  require Logger\n\n  def handle_event([:con_cache, :stats, :hit], _measurements, %{cache: %{name: cache_name}}, _config) do\n    # ... aggregate hits\n  end\n\n  def handle_event([:con_cache, :stats, :miss], _measurements, %{cache: %{name: cache_name}}, _config) do\n    # ... aggregate misses\n  end\nend\n```\n\n## Supervision\n\nA call to `ConCache.start_link` (or `start`) creates the so called _cache owner process_. This is the process that is the owner of the underlying ETS table and also the process where TTL checks are performed. No other operation (such as get or put) runs in this process.\n\nAs you've seen from the examples above, it's your responsibility to place the cache owner process into your own supervision tree. This gives you the control of cache cleanup when some subtree terminates (since a termination of the owner process will release the ETS table).\n\nIf for some reason `:con_cache` application is terminated, all cache owner processes will be terminated as well, regardless of the fact that they do not reside in the `:con_cache` supervision tree.\n\n### Multiple caches\n\nSometimes it can be useful to run multiple caches - say, if you need 2 caches with different global expiry values. Even though you can override ttl for each\nitem individually, it might get tedious very quickly.\n\nBy default it's not possible to run multiple caches under the same supervisor because child specification of each cache owner process has `id` equal to `ConCache`.\n\nHowever you can override default child specification and provide unique `id`:\n\n```elixir\ndef start(_type, _args) do\n  Supervisor.start_link(\n    [\n      ...\n      con_cache_child_spec(:my_cache_1, 100),\n      con_cache_child_spec(:my_cache_2, 200)\n      ...\n    ],\n    ...\n  )\nend\n\ndefp con_cache_child_spec(name, global_ttl) do\n  Supervisor.child_spec(\n    {\n      ConCache,\n      [\n        name: name,\n        ttl_check_interval: :timer.seconds(1),\n        global_ttl: :timer.seconds(global_ttl)\n      ]\n    },\n    id: {ConCache, name}\n  )\nend\n```\n\nSee [Supervisor.child_spec/2](https://hexdocs.pm/elixir/Supervisor.html#child_spec/2-examples) for details of this technique.\n\n## Process alias\n\nFunctions `ConCache.start` and `ConCache.start_link` return standard `{:ok, pid}` result. You can interface with the cache using this pid. As mentioned, cache operations are not running through this process - the pid is just used to discover the corresponding ETS table.\n\nMost of the time using pid to interface the cache is not appropriate. Just like in examples above, you usually want to give some alias to your cache, and then access it via this alias. In the examples above, we used `name: :some_alias` to provide local alias. Alternatively, you can use following formats for `name` option:\n\n```elixir\n{:global, some_alias}         # globally registered alias\n{:via, module, some_alias}    # registered through some module (e.g. gproc)\n```\n\nIn this case, you can just pass the same tuple to other `ConCache` functions. For example, to use the cache with [gproc](https://github.com/uwiger/gproc), you can do something like this:\n\n```elixir\nConCache.start_link([], name: {:via, :gproc, :my_cache})\n...\nConCache.put({:via, :gproc, :my_cache}, :some_key, :some_value)\n```\n\n## Testing in your application\n\nKeep in mind that `ConCache` introduces a state to your system. Thus, when you're testing your application, some tests might accidentally compromise the execution of other tests. There are a couple of options to work around that:\n\n1. Use different keys in each test. This could help avoiding tests compromising each other.\n1. Before each test, force restart the `ConCache` process. This will ensure each test runs with the empty cache.\n\n  ```elixir\n  setup do\n    Supervisor.terminate_child(con_cache_supervisor, ConCache)\n    Supervisor.restart_child(con_cache_supervisor, ConCache)\n    :ok\n  end\n  ```\n\n  Where `con_cache_supervisor` is the supervisor from which the `ConCache` process is started.\n\n3. Fetch all keys from the `ets` table, and delete each entry:\n\n  ```elixir\n  setup do\n    :my_cache\n    |\u003e ConCache.ets\n    |\u003e :ets.tab2list\n    |\u003e Enum.each(fn({key, _}) -\u003e ConCache.delete(:my_cache, key) end)\n\n    :ok\n  end\n  ```\n\n## Inner workings\n\n### ETS table\n\nThe ETS table is always public, and by default it is of _set_ type. Some ETS parameters can be changed:\n\n```elixir\nConCache.start_link(ets_options: [\n  :named_table,\n  {:name, :test_name},\n  :ordered_set,\n  {:read_concurrency, true},\n  {:write_concurrency, true},\n  {:decentralized_counters, true},\n  {:heir, heir_pid}\n])\n```\n\nAdditionally, you can override ConCache, and access ETS directly:\n\n```elixir\n:ets.insert(ConCache.ets(cache), {key, value})\n```\n\nOf course, this completely overrides additional ConCache behavior, such as ttl, row locking and callbacks.\n\n#### Bag and Duplicate Bag\n\nThose types are now supported by ConCache but like ETS, some functions are not supported by those types. Here are the list of functions **not** supported by bag and duplicate bag type tables:\n\n- `update/3`\n- `dirty_update/3`\n- `update_existing/3`\n- `dirty_update_existing/3`\n- `get_or_store/3`\n- `dirty_get_or_store/3`\n- `fetch_or_store/3`\n- `dirty_fetch_or_store/3`\n\n### Locking\n\nTo provide isolation, custom implementation of mutex is developed. This enables that each update operation is executed in the caller process, without the need to send data to another sync process.\n\nWhen a modification operation is called, the ConCache first acquires the lock and then performs the operation. The acquiring is done using the pool of lock processes that reside in the ConCache supervision tree. The pool contains as many processes as there are schedulers.\n\nIf the lock is not acquired in a predefined time (default = 5 seconds, alter with _acquire_lock_timeout_ ConCache parameter) an exception will be generated.\n\nYou can use explicit isolation to perform isolated reads if needed. In addition, you can use your own lock ids to implement bigger granularity:\n\n```elixir\nConCache.isolated(cache, key, fn() -\u003e\n  ConCache.get(cache, key)    # isolated read\nend)\n\n# Operation isolated on an arbitrary id. The id doesn't have to correspond to a cache item.\nConCache.isolated(cache, my_lock_id, fn() -\u003e\n  ...\nend)\n\n# Same as above, but immediately returns {:error, :locked} if lock could not be acquired.\nConCache.try_isolated(cache, my_lock_id, fn() -\u003e\n  ...\nend)\n```\n\nKeep in mind that these calls are isolated, but not transactional (atomic). Once something is modified, it is stored in ETS regardless of whether the remaining calls succeed or fail.\nThe isolation operations can be arbitrarily nested, although I wouldn't recommend this approach.\n\n### TTL\n\nWhen ttl is configured, the owner process works in discrete steps using `:erlang.send_after` to trigger the next step.\n\nWhen an item ttl is set, the owner process receives a message and stores it in its internal structure without doing anything else. Therefore, repeated touching of items is not very expensive.\n\nIn the next discrete step, the owner process first applies the pending ttl set requests to its internal state. Then it checks which items must expire at this step, purges them, and calls `:erlang.send_after` to trigger the next step.\n\nThis approach allows the owner process to do fairly small amount of work in each discrete step.\n\n### Consequences\n\nDue to the locking and ttl algorithms just described, some additional processing will occur in the owner processes. The work is fairly optimized, but I didn't invest too much time in it.\nFor example, lock processes currently use pure functional structures such as `HashDict` and `:gb_trees`. This could probably be replaced with internal ETS table to make it work faster, but I didn't try it.\n\nDue to locking and ttl inner workings, multiple copies of each key exist in memory. Therefore, I recommend avoiding complex keys.\n\n## Status\n\nConCache has been used in production to manage several thousands of entries served to up to 4000 concurrent clients, on the load of up to 2000 reqs/sec. I don't maintain that project anymore, so I'm not aware of its current status.\n\n## Copyright and License\n\nCopyright (c) 2013 Saša Jurić\n\nReleased under the MIT License, which can be found in the repository in [`LICENSE`](https://github.com/sasa1977/con_cache/blob/master/LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsasa1977%2Fcon_cache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsasa1977%2Fcon_cache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsasa1977%2Fcon_cache/lists"}