{"id":20763779,"url":"https://github.com/revenuecat/meta-memcache-py","last_synced_at":"2025-08-03T06:42:12.317Z","repository":{"id":37950514,"uuid":"436792317","full_name":"RevenueCat/meta-memcache-py","owner":"RevenueCat","description":"Modern, pure python, memcache client with support for new meta commands.","archived":false,"fork":false,"pushed_at":"2024-11-27T22:11:31.000Z","size":287,"stargazers_count":20,"open_issues_count":5,"forks_count":0,"subscribers_count":13,"default_branch":"main","last_synced_at":"2024-12-13T05:36:10.685Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","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/RevenueCat.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":"2021-12-09T23:37:34.000Z","updated_at":"2024-11-28T08:55:16.000Z","dependencies_parsed_at":"2023-01-21T12:17:40.186Z","dependency_job_id":"9fa75eb2-ba07-4539-b37f-e2bd297ac2cd","html_url":"https://github.com/RevenueCat/meta-memcache-py","commit_stats":{"total_commits":41,"total_committers":3,"mean_commits":"13.666666666666666","dds":"0.41463414634146345","last_synced_commit":"f5b3714cc145d77832e3aa2e6b3d4e04c902ad90"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fmeta-memcache-py","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fmeta-memcache-py/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fmeta-memcache-py/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RevenueCat%2Fmeta-memcache-py/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RevenueCat","download_url":"https://codeload.github.com/RevenueCat/meta-memcache-py/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230542287,"owners_count":18242332,"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-11-17T10:47:31.721Z","updated_at":"2024-12-20T06:05:45.598Z","avatar_url":"https://github.com/RevenueCat.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# meta-memcache-py\nModern, pure python, memcache client with support for new meta commands.\n\n# Usage:\n\n## Install:\n```\npip install meta-memcache\n```\n## Configure a client:\n```python:\nfrom meta_memcache import (\n    Key,\n    ServerAddress,\n    CacheClient,\n    connection_pool_factory_builder,\n)\n\npool = CacheClient.cache_client_from_servers(\n    servers=[\n        ServerAddress(host=\"1.1.1.1\", port=11211),\n        ServerAddress(host=\"2.2.2.2\", port=11211),\n        ServerAddress(host=\"3.3.3.3\", port=11211),\n    ],\n    connection_pool_factory_fn=connection_pool_factory_builder(),\n)\n```\n\nThe design is very pluggable. Rather than supporting a lot of features, it\nrelies on dependency injection to configure behavior.\n\nThe `CacheClient.cache_client_from_servers`s expects a `connection_pool_factory_fn`\ncallback to build the internal connection pool. And the connection pool receives\na function to create a new memcache connection.\n\nWhile this is very flexible, it can be complex to initialize, so there is a\ndefault builder provided to tune the most frequent things:\n```\ndef connection_pool_factory_builder(\n    initial_pool_size: int = 1,\n    max_pool_size: int = 3,\n    mark_down_period_s: float = DEFAULT_MARK_DOWN_PERIOD_S,\n    connection_timeout: float = 1,\n    recv_timeout: float = 1,\n    no_delay: bool = True,\n    read_buffer_size: int = 4096,\n) -\u003e Callable[[ServerAddress], ConnectionPool]:\n```\n* `initial_pool_size`: How many connections open for each host in the pool\n* `max_pool_size`: Maximum number of connections to keep open for each host in\n  the pool. Note that if there are no connections available in the pool, the\n  client will open a new connection always, instead of just blocking waiting\n  for a free connection. If you see too many connection creations in the\n  stats, you might need to increase this setting.\n* `mark_down_period_s`: When a network failure is detected, the destination host\n  is marked down, and requests will fail fast, instead of trying to reconnect\n  causing clients to block. A single client request will be checking if the host\n  is still down every `mark_down_period_s`, while the others fail fast.\n* `connection_timeout`: Timeout to stablish initial connection, ideally should\n  be \u003c 1 s for memcache servers in local network.\n* `recv_timeout`: Timeout of requests. Ideally should be \u003c 1 s for memcache\n  servers in local network.\n* `no_delay`: Wether to configure socket with NO_DELAY. This library tries to\n  send requests as a single write(), so enabling no_delay is a good idea.\n* `read_buffer_size`: This client tries to minimize memory allocation by reading\n  bytes from the socket into a reusable read buffer. If the memcache response\n  size is \u003c `read_buffer_size` no memory allocations happen for the network\n  read. Note: Each connection will have its own read buffer, so you must find a\n  good balance between memory usage and reducing memory allocations. Note: While\n  reading from the socket has zero allocations, the values will be deserialized\n  and those will have the expected memory allocations.\n\nIf you need to customize how the sockets are created (IPv6, add auth, unix\nsockets) you will need to implement your own `connection_pool_factory_builder`\nand override the `socket_factory_fn`.\n\n## Use the pool:\n```python:\ncache_client.set(key=Key(\"bar\"), value=1, ttl=1000)\n```\n\n## Keys:\n### String or `Key` named tuple\nOn the high-level commands you can use either plain strings as keys\nor the more advanced `Key` object that allows extra features like\ncustom routing and unicode keys.\n\n### Custom routing:\nYou can control the routing of the keys setting a custom `routing_key`:\n```python:\nKey(\"key:1:2\", routing_key=\"key:1\")\n```\nThis is useful if you have several keys related with each other. You can use the\nsame routing key, and they will be placed in the same server. This is handy for\nspeed of retrieval (single request instead of fan-out) and/or consistency (all\nwill be gone or present, since they are stored in the same server). Note this is\nalso risky, if you place all keys of a user in the same server, and the server\ngoes down, the user life will be miserable.\n\n### Custom domains:\nYou can add a domain to keys. This domain can be used for custom per-domain\nmetrics like hit ratios or to control serialization of the values.\n```python:\nKey(\"key:1:2\", domain=\"example\")\n```\nFor example the ZstdSerializer allows to configure different dictionaries by\ndomain, so you can compress more efficiently data of different domains.\n\n### Unicode/binary keys:\nBoth unicode and binary keys are supported, the keys will be hashed/encoded according to Meta commands\n[binary encoded keys](https://github.com/memcached/memcached/wiki/MetaCommands#binary-encoded-keys)\nspecification.\n\nUsing binary keys can have benefits, saving space in memory. While over the wire the key\nis transmited b64 encoded, the memcache server will use the byte representation, so it will\nnot have the 1/4 overhead of b64 encoding.\n\n```python:\nKey(\"🍺\")\n```\n\n### Large keys:\nLarge keys are also automatically supported and binary encoded as above. But\ndon't use large keys :)\n\n# Design:\nThe code relies on dependency injection to allow to configure a lot of the\naspects of the cache client. For example, instead of supporting a lot of\nfeatures on how to connect, authenticate, etc, a `socket_factory_fn` is required\nand you can customize the socket creation to your needs. We provide some basic\nsane defaults, but you should not have a lot of issues to customize it for your\nneeds.\n\nRegarding cache client features, relies in inheritance to abstract different\nlayers of responsibility, augment the capabilities while abstracting details\nout:\n\n## Architecture\nCode should rely on the [`CacheApi`](https://github.com/RevenueCat/meta-memcache-py/blob/main/src/meta_memcache/interfaces/cache_api.py) prototol.\n\nThe client itself is just two mixins that implement meta-commands and high-level commands.\n\nThe client uses a `Router` to execute the commands. Routers are reponsible of routing\nthe request to the appropriate place.\n\nBy default a client has a single Cache pool defined, with each of the servers\nhaving its own connection pool. The Router will map the key to the appropriate\nserver's connection pool. It can also implement more complex routing policies\nlike shadowing and mirroring.\n\nThe Router uses the connection pool and the executor to execute the command and read the\nresponse.\n\nBy mixing and plugging routers and executors is possible to build advanced\nbehaviors. Check [`CacheClient`](https://github.com/RevenueCat/meta-memcache-py/blob/main/src/meta_memcache/cache_client.py) and [`extras/`](https://github.com/RevenueCat/meta-memcache-py/blob/main/src/meta_memcache/extrasy) for some examples.\n\n## Low level meta commands:\n\nThe low-level commands are in\n[BaseCacheClient](https://github.com/RevenueCat/meta-memcache-py/blob/main/src/meta_memcache/commands/meta_commands.py).\n\nThey implement the new\n[Memcache MetaCommands](https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L79):\nmeta get, meta set, meta delete and meta arithmetic. They implement the full set\nof flags, and features, but are very low level for general use.\n\n```python:\n    def meta_multiget(\n        self,\n        keys: List[Key],\n        flags: Optional[RequestFlags] = None,\n        failure_handling: FailureHandling = DEFAULT_FAILURE_HANDLING,\n    ) -\u003e Dict[Key, ReadResponse]:\n\n    def meta_get(\n        self,\n        key: Key,\n        flags: Optional[RequestFlags] = None,\n        failure_handling: FailureHandling = DEFAULT_FAILURE_HANDLING,\n    ) -\u003e ReadResponse:\n\n    def meta_set(\n        self,\n        key: Key,\n        value: Any,\n        ttl: int,\n        flags: Optional[RequestFlags] = None,\n        failure_handling: FailureHandling = DEFAULT_FAILURE_HANDLING,\n    ) -\u003e WriteResponse:\n\n    def meta_delete(\n        self,\n        key: Key,\n        flags: Optional[RequestFlags] = None,\n        failure_handling: FailureHandling = DEFAULT_FAILURE_HANDLING,\n    ) -\u003e WriteResponse:\n\n    def meta_arithmetic(\n        self,\n        key: Key,\n        flags: Optional[RequestFlags] = None,\n        failure_handling: FailureHandling = DEFAULT_FAILURE_HANDLING,\n    ) -\u003e WriteResponse:\n```\n### Special arguments:\n`RequestFlags` has the following arguments:\n * `no_reply`: Set to True if the server should not send a response\n * `return_client_flag`: Set to True if the server should return the client flag\n * `return_cas_token`: Set to True if the server should return the CAS token\n * `return_value`: Set to True if the server should return the value (Default)\n * `return_ttl`: Set to True if the server should return the TTL\n * `return_size`: Set to True if the server should return the size (useful if when paired with return_value=False, to get the size of the value)\n * `return_last_access`: Set to True if the server should return the last access time\n * `return_fetched`: Set to True if the server should return the fetched flag\n * `return_key`: Set to True if the server should return the key in the response\n * `no_update_lru`: Set to True if the server should not update the LRU on this access\n * `mark_stale`: Set to True if the server should mark the value as stale\n * `cache_ttl`: The TTL to set on the key\n * `recache_ttl`: The TTL to use for recache policy\n * `vivify_on_miss_ttl`: The TTL to use when vivifying a value on a miss\n * `client_flag`: The client flag to store along the value (Useful to store value type, compression, etc)\n * `ma_initial_value`: For arithmetic operations, the initial value to use (if the key does not exist)\n * `ma_delta_value`: For arithmetic operations, the delta value to use\n * `cas_token`: The CAS token to use when storing the value in the cache\n * `opaque`: The opaque flag (will be echoed back in the response)\n * `mode`: The mode to use when storing the value in the cache. See SET_MODE_* and MA_MODE_* constants\n\n`FailureHandling` controls how the failures are handled. Has the arguments:\n * `raise_on_server_error`: (`Optional[bool]`) Wether to raise on error:\n   - `True`: Raises on server errors\n   - `False`: Returns miss for reads and false on writes\n   - `None` (DEFAULT): Use the raise on error setting configured in the Router\n * `track_write_failures``: (`bool`) Wether to track failures:\n   - `True` (DEFAULT): Track write failures\n   - `False`: Do not notify write failures\n\nThe default settings are usually good, but there are situations when you want control.\nFor example, a refill (populating an entry that was missing on cache) doesn't need to\ntrack write failures. If fails to be written, the cache will still be empty, so no need\nto track that as a write failure. Similarly sometimes you need to know if a write failed\ndue to CAS semantics, or because it was an add vs when it is due to server failure.\n\n### Responses:\nThe responses are either:\n * `ReadResponse`: `Union[Miss, Value, Success]`\n * `WriteResponse`:  `Union[Success, NotStored, Conflict, Miss]`\n\nWhich are:\n * `Miss`: For key not found. No arguments\n * `Success`: Successfull operation\n   - `flags`: `ResponseFlags` \n * `Value`: For value responses\n   - `flags`: `ResponseFlags` \n   - `size`: `int` Size of the value\n   - `value`: `Any` The value\n * `NotStored`: Not stored, for example \"add\" on exising key. No arguments.\n * `Conflict`: Not stored, for example due to CAS mismatch. No arguments.\n\nThe `ResponseFlags` contains the all the returned flags. This metadata gives a lot of\ncontrol and posibilities, it is the strength of the meta protocol:\n * `cas_token`: Compare-And-Swap token (integer value) or `None` if not returned\n * `fetched`:\n     - `True` if fetched since being set\n     - `False` if not fetched since being set\n     - `None` if the server did not return this flag info\n * `last_access`: time in seconds since last access (integer value) or `None` if not returned\n * `ttl`: time in seconds until the value expires (integer value) or `None` if not returned\n     - The special value `-1` represents if the key will never expire\n * `client_flag`: integer value or `None` if not returned\n * `win`:\n     - `True` if the client won the right to repopulate\n     - `False` if the client lost the right to repopulate\n     - `None` if the server did not return a win/lose flag\n * `stale`: `True` if the value is stale, `False` otherwise\n * `real_size`: integer value or `None` if not returned\n * `opaque flag`: bytes value or `None` if not returned\n\nNOTE: You shouldn't use this api directly, unless you are implementing some custom high-level\ncommand. See below for the usual memcache api.\n\n## High level commands:\n\nThe\n[High level commands](https://github.com/RevenueCat/meta-memcache-py/blob/main/src/meta_memcache/commands/high_level_commands.py)\naugments the low-level api and implement the more usual, high-level memcache\noperations (get, set, touch, cas...), plus the memcached's\n[new MetaCommands anti-dogpiling techniques](https://github.com/memcached/memcached/wiki/MetaCommands)\nfor high qps caching needs: Atomic Stampeding control, Herd Handling, Early\nRecache, Serve Stale, No Reply, Probabilistic Hot Cache, Hot Key Cache\nInvalidation...\n\n```python:\n    def set(\n        self,\n        key: Union[Key, str],\n        value: Any,\n        ttl: int,\n        no_reply: bool = False,\n        cas_token: Optional[int] = None,\n        stale_policy: Optional[StalePolicy] = None,\n        set_mode: SetMode = SetMode.SET,  # Other are ADD, REPLACE, APPEND...\n    ) -\u003e bool:\n        \"\"\"\n        Write a value using the specified `set_mode`\n        \"\"\"\n\n    def refill(\n        self: HighLevelCommandMixinWithMetaCommands,\n        key: Union[Key, str],\n        value: Any,\n        ttl: int,\n        no_reply: bool = False,\n    ) -\u003e bool:\n        \"\"\"\n        Try to refill a value.\n\n        Use this method when you got a cache miss, read from DB and\n        are trying to refill the value.\n\n        DO NOT USE to write new state.\n\n        It will:\n         * use \"ADD\" mode, so it will fail if the value is already\n           present in cache.\n         * It will also disable write failure tracking. The write\n           failure tracking is often used to invalidate keys that\n           fail to be written. Since this is not writting new state,\n           there is no need to track failures.\n        \"\"\"\n\n    def delete(\n        self,\n        key: Union[Key, str],\n        cas_token: Optional[int] = None,\n        no_reply: bool = False,\n        stale_policy: Optional[StalePolicy] = None,\n    ) -\u003e bool:\n        \"\"\"\n        Returns True if the key existed and it was deleted.\n        If the key is not found in the cache it will return False. If\n        you just want to the key to be deleted not caring of whether\n        it exists or not, use invalidate() instead.\n        \"\"\"\n\n    def invalidate(\n        self,\n        key: Union[Key, str],\n        cas_token: Optional[int] = None,\n        no_reply: bool = False,\n        stale_policy: Optional[StalePolicy] = None,\n    ) -\u003e bool:\n        \"\"\"\n        Returns true of the key deleted or it didn't exist anyway\n        \"\"\"\n\n    def touch(\n        self,\n        key: Union[Key, str],\n        ttl: int,\n        no_reply: bool = False,\n    ) -\u003e bool:\n        \"\"\"\n        Modify the TTL of a key without retrieving the value\n        \"\"\"\n\n    def get_or_lease(\n        self,\n        key: Union[Key, str],\n        lease_policy: LeasePolicy,\n        touch_ttl: Optional[int] = None,\n        recache_policy: Optional[RecachePolicy] = None,\n    ) -\u003e Optional[Any]:\n        \"\"\"\n        Get a key. On miss try to get a lease.\n\n        Guarantees only one cache client will get the miss and\n        gets to repopulate cache, while the others are blocked\n        waiting (according to the settings in the LeasePolicy)\n        \"\"\"\n\n    def get_or_lease_cas(\n        self,\n        key: Union[Key, str],\n        lease_policy: LeasePolicy,\n        touch_ttl: Optional[int] = None,\n        recache_policy: Optional[RecachePolicy] = None,\n    ) -\u003e Tuple[Optional[Any], Optional[int]]:\n        \"\"\"\n        Same as get_or_lease(), but also return the CAS token so\n        it can be used during writes and detect races\n        \"\"\"\n\n    def get(\n        self,\n        key: Union[Key, str],\n        touch_ttl: Optional[int] = None,\n        recache_policy: Optional[RecachePolicy] = None,\n    ) -\u003e Optional[Any]:\n        \"\"\"\n        Get a key\n        \"\"\"\n\n    def multi_get(\n        self,\n        keys: List[Union[Key, str]],\n        touch_ttl: Optional[int] = None,\n        recache_policy: Optional[RecachePolicy] = None,\n    ) -\u003e Dict[Key, Optional[Any]]:\n        \"\"\"\n        Get multiple keys at once\n        \"\"\"\n\n    def get_cas(\n        self,\n        key: Union[Key, str],\n        touch_ttl: Optional[int] = None,\n        recache_policy: Optional[RecachePolicy] = None,\n    ) -\u003e Tuple[Optional[Any], Optional[int]]:\n        \"\"\"\n        Same as get(), but also return the CAS token so\n        it can be used during writes and detect races\n        \"\"\"\n\n    def get_typed(\n        self,\n        key: Union[Key, str],\n        cls: Type[T],\n        touch_ttl: Optional[int] = None,\n        recache_policy: Optional[RecachePolicy] = None,\n        error_on_type_mismatch: bool = False,\n    ) -\u003e Optional[T]:\n        \"\"\"\n        Same as get(), but ensure the type matched the provided cls\n        \"\"\"\n\n    def get_cas_typed(\n        self,\n        key: Union[Key, str],\n        cls: Type[T],\n        touch_ttl: Optional[int] = None,\n        recache_policy: Optional[RecachePolicy] = None,\n        error_on_type_mismatch: bool = False,\n    ) -\u003e Tuple[Optional[T], Optional[int]]:\n        \"\"\"\n        Same as get_typed(), but also return the CAS token so\n        it can be used during writes and detect races\n        \"\"\"\n\n    def delta(\n        self,\n        key: Union[Key, str],\n        delta: int,\n        refresh_ttl: Optional[int] = None,\n        no_reply: bool = False,\n        cas_token: Optional[int] = None,\n    ) -\u003e bool:\n        \"\"\"\n        Increment/Decrement a key that contains a counter\n        \"\"\"\n\n    def delta_initialize(\n        self,\n        key: Union[Key, str],\n        delta: int,\n        initial_value: int,\n        initial_ttl: int,\n        refresh_ttl: Optional[int] = None,\n        no_reply: bool = False,\n        cas_token: Optional[int] = None,\n    ) -\u003e bool:\n        \"\"\"\n        Increment/Decrement a key that contains a counter,\n        creating and setting it to the initial value if the\n        counter does not exist.\n        \"\"\"\n\n    def delta_and_get(\n        self,\n        key: Union[Key, str],\n        delta: int,\n        refresh_ttl: Optional[int] = None,\n        cas_token: Optional[int] = None,\n    ) -\u003e Optional[int]:\n        \"\"\"\n        Same as delta(), but return the resulting value\n        \"\"\"\n\n    def delta_initialize_and_get(\n        self,\n        key: Union[Key, str],\n        delta: int,\n        initial_value: int,\n        initial_ttl: int,\n        refresh_ttl: Optional[int] = None,\n        cas_token: Optional[int] = None,\n    ) -\u003e Optional[int]:\n        \"\"\"\n        Same as delta_initialize(), but return the resulting value\n        \"\"\"\n```\n\n# Reliability, consistency and best practices\nWe have published a deep-dive into some of the techniques to keep\ncache consistent and reliable under high load that RevenueCat uses,\navailable thanks to this cache client.\n\nSee: https://www.revenuecat.com/blog/engineering/data-caching-revenuecat/\n\n## Anti-dogpiling, preventing thundering herds:\nSome commands receive `RecachePolicy`, `StalePolicy` and `LeasePolicy` for the\nadvanced anti-dogpiling control needed in high-qps environments:\n\n```python:\nclass RecachePolicy(NamedTuple):\n    \"\"\"\n    This controls the recache herd control behavior\n    If recache ttl is indicated, when remaining ttl is \u003c given value\n    one of the clients will win, return a miss and will populate the\n    value, while the other clients will loose and continue to use the\n    stale value.\n    \"\"\"\n\n    ttl: int = 30\n\n\nclass LeasePolicy(NamedTuple):\n    \"\"\"\n    This controls the lease or miss herd control behavior\n    If miss lease retries \u003e 0, on misses a lease will be created. The\n    winner will get a Miss and will continue to populate the cache,\n    while the others are BLOCKED! Use with caution! You can define\n    how many times and how often clients will retry to get the\n    value. After the retries are expired, clients will get a Miss\n    if they weren't able to get the value.\n    \"\"\"\n\n    ttl: int = 30\n    miss_retries: int = 3\n    miss_retry_wait: float = 1.0\n    wait_backoff_factor: float = 1.2\n    miss_max_retry_wait: float = 5.0\n\n\nclass StalePolicy(NamedTuple):\n    \"\"\"\n    This controls the stale herd control behavior\n    * Deletions can mark items stale instead of deleting them\n    * Stale items automatically do recache control, one client\n      will get the miss, others will receive the stale value\n      until the winner refreshes the value in the cache.\n    * cas mismatches (due to race / further invalidation) can\n      store the value as stale instead of failing\n    \"\"\"\n\n    mark_stale_on_deletion_ttl: int = 0  # 0 means disabled\n    mark_stale_on_cas_mismatch: bool = False\n``` \n\n### Notes:\n* Recache/Stale policies are typically used together. Make sure all your reads\n  for a given key share the same recache policy to avoid unexpected behaviors.\n* Leases are for a more traditional, more consistent model, where other clients\n  will block instead of getting a stale value.\n\n## Pool level features:\nFinally in\n[`cache_client.py`](https://github.com/RevenueCat/meta-memcache-py/blob/main/src/meta_memcache/cache_client.py)\nhelps building different `CacheClient`s with different characteristics:\n* `cache_client_from_servers`: Implements the default, shardedconsistent hashing\n  cache pool using uhashring's `HashRing`.\n* `cache_client_with_gutter_from_servers`: implements a sharded cache pool like\n  above, but with a 'gutter pool' (See\n  [Scaling Memcache at Facebook](http://www.cs.utah.edu/~stutsman/cs6963/public/papers/memcached.pdf)),\n  so when a server of the primary pool is down, requests are sent to the\n  'gutter' pool, with TTLs overriden and lowered on the fly, so they provide\n  some level of caching instead of hitting the backend for each request.\n\nThese clients also provide an option to register a callback for write failure events. This might be useful\nif you are serious about cache consistency. If you have transient network issues, some writes might fail,\nand if the server comes back without being restarted or the cache flushed, the data will be stale. The\nevents allows for failed writes to be collected and logged. Affected keys can then be invalidated later\nand eventual cache consistency guaranteed.\n\nIt should be trivial to implement your own cache client if you need custom\nsharding, shadowing, pools that support live migrations, etc. Feel free to\ncontribute!\n\n## Write failure tracking\nWhen a write failure occures with a `SET` or `DELETE` opperation occures then the `cache_client.on_write_failure` event\nhandler will be triggered. Consumers subscribing to this handler will receive the key that failed. The\nfollowing is an example on how to subscribe to these events:\n```python:\nfrom meta_memcache import CacheClient, Key\n\nclass SomeConsumer(object):\n    def __init__(self, cache_client: CacheClient):\n        self.cache_client = cache_client\n        self.cache_client.on_write_failures += self.on_write_failure_handler\n\n    def call_before_dereferencing(self):\n        self.cache_client.on_write_failures -= self.on_write_failure_handler\n\n    def on_write_failure_handler(self, key: Key) -\u003e None:\n        # Handle the failures here\n        pass\n```\n\nIdeally you should track such errors and attempt to invalidate the affected keys\nlater, when the cache server is back, so you keep high cache consistency, avoiding\nstale entries.\n\n## Stats:\nThe cache clients offer a `get_counters()` that return information about the state\nof the servers and their connection pools:\n\n```python:\n    def get_counters(self) -\u003e Dict[ServerAddress, PoolCounters]:\n```\n\nThe counters are:\n```python:\nclass PoolCounters(NamedTuple):\n    # Available connections in the pool, ready to use\n    available: int\n    # The # of connections active, currently in use, out of the pool\n    active: int\n    # Current stablished connections (available + active)\n    stablished: int\n    # Total # of connections created. If this keeps growing\n    # might mean the pool size is too small and we are\n    # constantly needing to create new connections:\n    total_created: int\n    # Total # of connection or socket errors\n    total_errors: int\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frevenuecat%2Fmeta-memcache-py","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frevenuecat%2Fmeta-memcache-py","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frevenuecat%2Fmeta-memcache-py/lists"}