ConCache Documentation

repository·master·Indexed 21 days ago

https://github.com/sasa1977/con_cache

An ETS-based key/value storage for OTP applications featuring row-level synchronized writes, TTL support, and modification callbacks. It provides atomic operations like put, update, and get_or_store, as well as high-performance non-synchronized access via dirty modifiers. ConCache includes support for isolated reads, custom locking granularity, and telemetry events for tracking cache hits and misses.

Tokens
5.6K
Snippets
21
Records
25
Agent score
74%

What's inside ConCache

  1. Configure cache process aliases

    master

    While you can use the PID returned by start_link, it is standard practice to use an alias. You can register the cache locally, globally, or via a module (like gproc).

    # Local registration
    ConCache.start_link([], name: :my_cache)
    
    # Global registration
    ConCache.start_link([], name: {:global, :my_cache})
    
    # Via module (e.g., gproc)
    ConCache.start_link([], name: {:via, :gproc, :my_cache})
    ConCache.put({:via, :gproc, :my_cache}, :some_key, :some_value)
  2. How TTL (Time To Live) works in ConCache

    master

    ConCache manages TTL using a discrete step approach with :erlang.send_after.

    1. When an item's TTL is set, the owner process receives a message and stores the request internally. Repeatedly 'touching' items is efficient because it only involves internal state updates.
    2. In discrete steps, the owner process applies pending TTL requests, purges expired items, and schedules the next step via :erlang.send_after.

    Performance Note: Because of the locking and TTL mechanisms, multiple copies of each key may exist in memory. It is recommended to avoid using complex keys.

  3. Start a ConCache instance in an OTP application

    master

    A cache is started using ConCache.start or ConCache.start_link. It is recommended to start the cache from a supervisor. You should provide a name option to register the cache under an alias, which allows you to interact with it without managing the PID directly.

    children = [
      {ConCache, [name: :my_cache, ttl_check_interval: false]}
      ...
    ]
    
    Supervisor.start_link(children, options)
  4. Configure ETS options for ConCache

    master

    When starting ConCache via ConCache.start_link/1, you can pass custom ets_options to configure the underlying ETS table. By default, the table is of type :set. You can specify parameters such as :named_table, :ordered_set, or concurrency settings like :read_concurrency and :write_concurrency.

    ConCache.start_link(ets_options: [
      :named_table,
      {:name, :test_name},
      :ordered_set,
      {:read_concurrency, true},
      {:write_concurrency, true},
      {:decentralized_counters, true},
      {:heir, heir_pid}
    ])
  5. Run multiple ConCache instances under one supervisor

    master

    By default, multiple caches cannot be started under the same supervisor because they share the same child ID (ConCache). To run multiple caches (e.g., with different global TTLs), you must provide a unique id in the child specification using Supervisor.child_spec/2.

    def start(_type, _args) do
      Supervisor.start_link(
        [
          ...
          con_cache_child_spec(:my_cache_1, 100),
          con_cache_child_spec(:my_cache_2, 200)
          ...
        ],
        ...
      )
    end
    
    defp con_cache_child_spec(name, global_ttl) do
      Supervisor.child_spec(
        {
          ConCache,
          [
            name: name,
            ttl_check_interval: :timer.seconds(1),
            global_ttl: :timer.seconds(global_ttl)
          ]
        },
        id: {ConCache, name}
      )
    end
  6. Configure Time-To-Live (TTL) settings

    master

    ConCache supports TTL for cache items. You can set a global TTL and a check interval for the background process that cleans up expired items.

    Key concepts:

    • ttl_check_interval: How often the background process checks for expired items. Recommended to be at least 1 second.
    • global_ttl: The default expiry for all items.
    • touch_on_read: If true, reading an item renews its TTL.
    • :infinity: Setting TTL to :infinity prevents an item from ever expiring.
    • ConCache.Item: Use this struct to set custom TTLs per item.
    # Global configuration
    {ConCache, [
      name: :my_cache,
      ttl_check_interval: :timer.seconds(1),
      global_ttl: :timer.seconds(5),
      touch_on_read: true
    ]}
    
    # Per-item custom TTL
    ConCache.put(:my_cache, :key, %ConCache.Item{value: "value", ttl: :timer.seconds(25)})
    
    # Update value without resetting TTL
    ConCache.put(:my_cache, :key, %ConCache.Item{value: "value", ttl: :no_update})
    
    # Manually renew TTL
    ConCache.touch(:my_cache, :key)
  7. Start a ConCache instance with start_link/1

    master

    To initialize a new cache, use ConCache.Owner.start_link(options). The options map allows you to configure the cache behavior, including TTL (Time To Live), ETS options, and naming.

    Supported configuration keys in options:

    • name: The name of the cache.
    • ttl: The default time-to-live for items (defaults to :infinity).
    • ttl_check: An integer specifying how often (in milliseconds) the TTL manager checks for expired items. If provided, it enables automatic expiration.
    • ets_options: A list of ETS configuration options (e.g., [:named_table, :compressed]).
    • acquire_lock_timeout: Timeout for acquiring locks (defaults to 5000).
    • touch_on_read: Boolean indicating if reading an item should refresh its TTL (defaults to false).
    • callback: A function to be called when an item expires.
    • time_size: Used to calculate max_time for the internal TTL loop.
    ConCache.Owner.start_link([
      name: :my_cache,
      ttl: 60_000,
      ttl_check: 1_000,
      ets_options: [:named_table]
    ])
  8. Use isolated reads and custom locks

    master

    ConCache uses a custom mutex implementation to provide isolation. While modification operations (update, put, delete) automatically acquire a lock on a per-row basis, you can use ConCache.isolated/3 to perform isolated reads or use arbitrary IDs to implement custom locking granularity.

    Important Notes:

    • Isolation is not transactional (atomic). If a series of calls within an isolated block fails, any modifications already made to ETS will persist.
    • Isolation operations can be nested, though it is not recommended.
    • If a lock cannot be acquired within the acquire_lock_timeout (default 5 seconds), an exception is raised. Use ConCache.try_isolated/3 to return {:error, :locked} immediately instead of raising an exception.
    # Perform an isolated read on a specific key
    ConCache.isolated(cache, key, fn() ->
      ConCache.get(cache, key)
    end)
    
    # Use an arbitrary ID for custom locking granularity
    ConCache.isolated(cache, my_lock_id, fn() ->
      ...
    end)
    
    # Attempt isolation but return error immediately if locked
    ConCache.try_isolated(cache, my_lock_id, fn() ->
      ...
    end)
  9. Perform synchronized updates and atomic stores

    master

    ConCache provides synchronized operations that are isolated on a row level. Modifications to a specific key (like update, put, or delete) will wait for the current operation on that key to finish, but other keys remain unaffected. Reads during an update are always dirty.

    # Update an existing key
    ConCache.update(:my_cache, :key, fn(old_value) ->
      {:ok, "new_value"}
    end)
    
    # Update only if the key exists; returns {:error, :not_existing} otherwise
    ConCache.update_existing(:my_cache, :key, fn(old_value) ->
      {:ok, "new_value"}
    end)
    
    # Returns existing value, or calls function and stores the result.
    # If multiple processes call this for the same key, the function runs only once.
    ConCache.get_or_store(:my_cache, :key, fn() ->
      "initial_value"
    end)
    
    # Similar to get_or_store, but only caches if the function returns an :ok tuple.
    ConCache.fetch_or_store(:my_cache, :key, fn ->
      case call_api() do
        {:ok, data} -> {:ok, process_data(data)}
        {:error, _reason} = error -> error
      end
    end)
  10. Use basic ConCache API operations

    master

    Once a cache is started with a name (e.g., :my_cache), you can perform standard key/value operations. Note that these requests run in the caller process and do not go through the started process.

    ConCache.put(:my_cache, :key, "value")         # inserts value or overwrites the old one
    ConCache.insert_new(:my_cache, :key, "value")  # inserts value or returns {:error, :already_exists}
    ConCache.get(:my_cache, :key)
    ConCache.delete(:my_cache, :key)
    ConCache.size(:my_cache)
  11. Access the underlying ETS table directly

    master

    You can bypass ConCache's logic (such as TTL, row locking, and callbacks) by accessing the ETS table directly using ConCache.ets(cache). Note that direct modifications like :ets.insert/2 will not trigger ConCache's management features.

    :ets.insert(ConCache.ets(cache), {key, value})