Caches

Once a Session has been obtained, it is now possible to begin working with NamedMap and/or NamedCache instances. This is done by calling either Session.get_map(name: str, cache_options: Optional[CacheOptions] = None) or Session.get_cache(name: str, cache_options: Optional[CacheOptions] = None). The name argument is the logical name for this cache. The optional cache_options argument accepts a CacheOptions to configure the default time-to-live (ttl) for entries placed in the cache as well as allowing the configuration of near caching (discussed later in this section).

Here are some examples:

# obtain NamedCache 'person'
cache: NamedCache[int, Person] = await session.get_cache("person")
# obtain NamedCache 'person' with a default ttl of 2000 millis
# any entry inserted into this cache, unless overridden by a put call
# with a custom ttl, will have a default ttl of 2000
options: CacheOptions = CacheOptions(2000)
cache: NamedCache[int, Person] = await session.get_cache("person", options)

Near Caches

Near caches are a local cache within the NamedMap or NamedCache that will store entries as they are obtained from the remote cache. By doing so, it is possible to reduce the number of remote calls made to the Coherence cluster by returning the locally cached value. This local cache also ensures updates made to, or removal of, an entry are properly reflected thus ensuring stale data isn’t mistakenly returned.

Note

Near caching will only work with Coherence CE 24.09 or later. Attempting to use near caching features with older versions will have no effect.

A near cache is configured via NearCacheOptions which provides several options for controlling how entries will be cached locally.

  • ttl - configures the time-to-live of locally cached entries (this has no impact on entries stored within Coherence). If not specified, or the ttl is 0, entries in the near cache will not expire

  • high_units - configures the max number of entries that may be locally

    cached. Once the number of locally cached entries exceeds the configured value, the cache will be pruned down (least recently used entries first) to a target size based on the configured prune_factor (defaults to 0.80 meaning the prune operation would retain 80% of the entries)

  • high_units_memory - configures the maximum memory size, in bytes, the

    locally cached entries may use. If total memory exceeds the configured value, the cache will be pruned down (least recently used entries first) to a target size based on the configured prune_factor (defaults to 0.80 meaning the prune operation would retain 80% the cache memory)

  • prune_factor - configures the target near cache size after exceeding

    either high_units or high_units_memory high-water marks

Note

high_units and high_units_memory are mutually exclusive

Examples of configuring near caching:

# obtain NamedCache 'person' and configure near caching with a local
# ttl of 20_000 millis
near_options: NearCacheOptions = NearCacheOptions(20_000)
cache_options: CacheOptions = CacheOptions(near_cache_options=near_options)
cache: NamedCache[int, Person] = await session.get_cache("person", options)
# obtain NamedCache 'person' and configure near caching with a max
# number of entries of 1_000 and when pruned, it will be reduced
# to 20%
near_options: NearCacheOptions = NearCacheOptions(high_units=1_000, prune_factor=0.20)
cache_options: CacheOptions = CacheOptions(near_cache_options=near_options)
cache: NamedCache[int, Person] = await session.get_cache("person", options)

To verify the effectiveness of a near cache, several statistics are monitored and may be obtained from the CacheStats instance returned by the near_cache_stats property of the NamedMap or NamedCache

The following statistics are available (the statistic name given is the same property name on the CacheStats instance)

  • hits - the number of times an entry was found in the near cache

  • misses - the number of times an entry was not found in the near cache

  • misses_duration - The accumulated time, in millis, spent for a cache miss (i.e., having to make a remote call and update the local cache)

  • hit_rate - the ratio of hits to misses

  • puts - the total number of puts that have been made against the near cache

  • gets - the total number of gets that have been made against the near cache

  • prunes - the number of times the cache was pruned due to exceeding the configured high_units or high_units_memory high-water marks

  • expires - the number of times the near cache’s expiry logic expired entries

  • num_pruned - the total number of entries that were removed due to exceeding the configured high_units or high_units_memory high-water marks

  • num_expired - the total number of entries that were removed due to expiration

  • prunes_duration - the accumulated time, in millis, spent pruning the near cache

  • expires_duration - the accumulated time, in millis, removing expired entries from the near cache

  • size - the total number of entries currently held by the near cache

  • bytes - the total bytes the near cache entries consume

Note

The near_cache_stats option will return None if near caching isn’t configured or available

The following example demonstrates the value that near caching can provide:

 1# Copyright (c) 2024, Oracle and/or its affiliates.
 2# Licensed under the Universal Permissive License v 1.0 as shown at
 3# https://oss.oracle.com/licenses/upl.
 4
 5import asyncio
 6import time
 7from functools import reduce
 8
 9from coherence import CacheOptions, CacheStats, NamedCache, NearCacheOptions, Session
10
11
12async def do_run() -> None:
13    session: Session = await Session.create()
14
15    # obtain the basic cache and configure near caching with
16    # all defaults; meaning no expiry or pruning
17    cache_remote: NamedCache[str, str] = await session.get_cache("remote")
18    cache_near: NamedCache[str, str] = await session.get_cache(
19        "near", CacheOptions(near_cache_options=NearCacheOptions(ttl=0))
20    )
21
22    await cache_remote.clear()
23    await cache_near.clear()
24    stats: CacheStats = cache_near.near_cache_stats
25
26    # these knobs control:
27    #  - how many current tasks to run
28    #  - how many entries will be inserted and queried
29    #  - how many times the calls will be invoked
30    task_count: int = 25
31    num_entries: int = 1_000
32    iterations: int = 4
33
34    # seed data to populate the cache
35    cache_seed: dict[str, str] = {str(x): str(x) for x in range(num_entries)}
36    cache_seed_keys: set[str] = {key for key in cache_seed.keys()}
37    print()
38
39    # task calling get_all() for 1_000 keys
40    async def get_all_task(task_cache: NamedCache[str, str]) -> int:
41        begin = time.time_ns()
42
43        for _ in range(iterations):
44            async for _ in await task_cache.get_all(cache_seed_keys):
45                continue
46
47        return (time.time_ns() - begin) // 1_000_000
48
49    await cache_remote.put_all(cache_seed)
50    await cache_near.put_all(cache_seed)
51
52    print("Run without near caching ...")
53    begin_outer: int = time.time_ns()
54    results: list[int] = await asyncio.gather(*[get_all_task(cache_remote) for _ in range(task_count)])
55    end_outer: int = time.time_ns()
56    total_time = end_outer - begin_outer
57    task_time = reduce(lambda first, second: first + second, results)
58
59    # Example output
60    # Run without near caching ...
61    # [remote] 25 tasks completed!
62    # [remote] Total time: 4246ms
63    # [remote] Tasks completion average: 3755.6
64
65    print(f"[remote] {task_count} tasks completed!")
66    print(f"[remote] Total time: {total_time // 1_000_000}ms")
67    print(f"[remote] Tasks completion average: {task_time / task_count}")
68
69    print()
70    print("Run with near caching ...")
71    begin_outer = time.time_ns()
72    results = await asyncio.gather(*[get_all_task(cache_near) for _ in range(task_count)])
73    end_outer = time.time_ns()
74    total_time = end_outer - begin_outer
75    task_time = reduce(lambda first, second: first + second, results)
76
77    # Run with near caching ...
78    # [near] 25 tasks completed!
79    # [near] Total time: 122ms
80    # [near] Tasks completion average: 113.96
81    # [near] Near cache statistics: CacheStats(puts=1000, gets=100000, hits=99000,
82    #     misses=1000, misses-duration=73ms, hit-rate=0.99, prunes=0,
83    #     num-pruned=0, prunes-duration=0ms, size=1000, expires=0,
84    #     num-expired=0, expires-duration=0ms, memory-bytes=681464)
85
86    print(f"[near] {task_count} tasks completed!")
87    print(f"[near] Total time: {total_time // 1_000_000}ms")
88    print(f"[near] Tasks completion average: {task_time / task_count}")
89    print(f"[near] Near cache statistics: {stats}")
90
91    await session.close()
92
93
94asyncio.run(do_run())