Skip to content

Commit 028a1a1

Browse files
authored
Merge pull request #174 from opentensor/feat/thewhaleking/add-cache-size-env-vars
Adds env var support for setting cache size
2 parents 456618d + 5abd398 commit 028a1a1

File tree

7 files changed

+119
-15
lines changed

7 files changed

+119
-15
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,41 @@ async def main():
5454
asyncio.run(main())
5555
```
5656

57+
### Caching
58+
There are a few different cache types used in this library to improve the performance overall. The one with which
59+
you are probably familiar is the typical `functools.lru_cache` used in `sync_substrate.SubstrateInterface`.
60+
61+
By default, it uses a max cache size of 512 for smaller returns, and 16 for larger ones. These cache sizes are
62+
user-configurable using the respective env vars, `SUBSTRATE_CACHE_METHOD_SIZE` and `SUBSTRATE_RUNTIME_CACHE_SIZE`.
63+
64+
They are applied only on methods whose results cannot change — such as the block hash for a given block number
65+
(small, 512 default), or the runtime for a given runtime version (large, 16 default).
66+
67+
Additionally, in `AsyncSubstrateInterface`, because of its asynchronous nature, we developed our own asyncio-friendly
68+
LRU caches. The primary one is the `CachedFetcher` which wraps the same methods as `functools.lru_cache` does in
69+
`SubstrateInterface`, but the key difference here is that each request is assigned a future that is returned when the
70+
initial request completes. So, if you were to do:
71+
72+
```python
73+
bn = 5000
74+
bh1, bh2 = await asyncio.gather(
75+
asi.get_block_hash(bn),
76+
asi.get_block_hash(bn)
77+
)
78+
```
79+
it would actually only make one single network call, and return the result to both requests. Like `SubstrateInterface`,
80+
it also takes the `SUBSTRATE_CACHE_METHOD_SIZE` and `SUBSTRATE_RUNTIME_CACHE_SIZE` vars to set cache size.
81+
82+
The third and final caching mechanism we use is `async_substrate_interface.async_substrate.DiskCachedAsyncSubstrateInterface`,
83+
which functions the same as the normal `AsyncSubstrateInterface`, but that also saves this cache to the disk, so the cache
84+
is preserved between runs. This is product for a fairly nice use-case (such as `btcli`). As you may call different networks
85+
with entirely different results, this cache is keyed by the uri supplied at instantiation of the `DiskCachedAsyncSubstrateInterface`
86+
object, so `DiskCachedAsyncSubstrateInterface(network_1)` and `DiskCachedAsyncSubstrateInterface(network_2)` will not share
87+
the same on-disk cache.
88+
89+
As with the other two caches, this also takes `SUBSTRATE_CACHE_METHOD_SIZE` and `SUBSTRATE_RUNTIME_CACHE_SIZE` env vars.
90+
91+
5792
## Contributing
5893

5994
Contributions are welcome! Please open an issue or submit a pull request to the `staging` branch.

async_substrate_interface/async_substrate.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import asyncio
88
import inspect
99
import logging
10+
import os
1011
import ssl
1112
import warnings
1213
from contextlib import suppress
@@ -42,7 +43,6 @@
4243
SubstrateRequestException,
4344
ExtrinsicNotFound,
4445
BlockNotFound,
45-
MaxRetriesExceeded,
4646
StateDiscardedError,
4747
)
4848
from async_substrate_interface.protocols import Keypair
@@ -81,6 +81,10 @@
8181
logger = logging.getLogger("async_substrate_interface")
8282
raw_websocket_logger = logging.getLogger("raw_websocket")
8383

84+
# env vars dictating the cache size of the cached methods
85+
SUBSTRATE_CACHE_METHOD_SIZE = int(os.getenv("SUBSTRATE_CACHE_METHOD_SIZE", "512"))
86+
SUBSTRATE_RUNTIME_CACHE_SIZE = int(os.getenv("SUBSTRATE_RUNTIME_CACHE_SIZE", "16"))
87+
8488

8589
class AsyncExtrinsicReceipt:
8690
"""
@@ -1178,7 +1182,7 @@ async def init_runtime(
11781182
else:
11791183
return await self.get_runtime_for_version(runtime_version, block_hash)
11801184

1181-
@cached_fetcher(max_size=16, cache_key_index=0)
1185+
@cached_fetcher(max_size=SUBSTRATE_RUNTIME_CACHE_SIZE, cache_key_index=0)
11821186
async def get_runtime_for_version(
11831187
self, runtime_version: int, block_hash: Optional[str] = None
11841188
) -> Runtime:
@@ -2111,7 +2115,7 @@ async def get_metadata(self, block_hash=None) -> MetadataV15:
21112115

21122116
return runtime.metadata_v15
21132117

2114-
@cached_fetcher(max_size=512)
2118+
@cached_fetcher(max_size=SUBSTRATE_CACHE_METHOD_SIZE)
21152119
async def get_parent_block_hash(self, block_hash) -> str:
21162120
"""
21172121
Retrieves the block hash of the parent of the given block hash
@@ -2166,7 +2170,7 @@ async def get_storage_by_key(self, block_hash: str, storage_key: str) -> Any:
21662170
"Unknown error occurred during retrieval of events"
21672171
)
21682172

2169-
@cached_fetcher(max_size=16)
2173+
@cached_fetcher(max_size=SUBSTRATE_RUNTIME_CACHE_SIZE)
21702174
async def get_block_runtime_info(self, block_hash: str) -> dict:
21712175
"""
21722176
Retrieve the runtime info of given block_hash
@@ -2179,7 +2183,7 @@ async def _get_block_runtime_info(self, block_hash: str) -> dict:
21792183
response = await self.rpc_request("state_getRuntimeVersion", [block_hash])
21802184
return response.get("result")
21812185

2182-
@cached_fetcher(max_size=512)
2186+
@cached_fetcher(max_size=SUBSTRATE_CACHE_METHOD_SIZE)
21832187
async def get_block_runtime_version_for(self, block_hash: str):
21842188
"""
21852189
Retrieve the runtime version of the parent of a given block_hash
@@ -2494,7 +2498,7 @@ async def rpc_request(
24942498
else:
24952499
raise SubstrateRequestException(result[payload_id][0])
24962500

2497-
@cached_fetcher(max_size=512)
2501+
@cached_fetcher(max_size=SUBSTRATE_CACHE_METHOD_SIZE)
24982502
async def get_block_hash(self, block_id: int) -> str:
24992503
"""
25002504
Retrieves the hash of the specified block number
@@ -4022,19 +4026,19 @@ class DiskCachedAsyncSubstrateInterface(AsyncSubstrateInterface):
40224026
Experimental new class that uses disk-caching in addition to memory-caching for the cached methods
40234027
"""
40244028

4025-
@async_sql_lru_cache(maxsize=512)
4029+
@async_sql_lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
40264030
async def get_parent_block_hash(self, block_hash):
40274031
return await self._get_parent_block_hash(block_hash)
40284032

4029-
@async_sql_lru_cache(maxsize=16)
4033+
@async_sql_lru_cache(maxsize=SUBSTRATE_RUNTIME_CACHE_SIZE)
40304034
async def get_block_runtime_info(self, block_hash: str) -> dict:
40314035
return await self._get_block_runtime_info(block_hash)
40324036

4033-
@async_sql_lru_cache(maxsize=512)
4037+
@async_sql_lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
40344038
async def get_block_runtime_version_for(self, block_hash: str):
40354039
return await self._get_block_runtime_version_for(block_hash)
40364040

4037-
@async_sql_lru_cache(maxsize=512)
4041+
@async_sql_lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
40384042
async def get_block_hash(self, block_id: int) -> str:
40394043
return await self._get_block_hash(block_id)
40404044

async_substrate_interface/sync_substrate.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
22
import logging
3+
import os
34
import socket
45
from hashlib import blake2b
56
from typing import Optional, Union, Callable, Any
@@ -55,6 +56,10 @@
5556
logger = logging.getLogger("async_substrate_interface")
5657
raw_websocket_logger = logging.getLogger("raw_websocket")
5758

59+
# env vars dictating the cache size of the cached methods
60+
SUBSTRATE_CACHE_METHOD_SIZE = int(os.getenv("SUBSTRATE_CACHE_METHOD_SIZE", "512"))
61+
SUBSTRATE_RUNTIME_CACHE_SIZE = int(os.getenv("SUBSTRATE_RUNTIME_CACHE_SIZE", "16"))
62+
5863

5964
class ExtrinsicReceipt:
6065
"""
@@ -813,6 +818,7 @@ def init_runtime(
813818
self.runtime = runtime
814819
return self.runtime
815820

821+
@functools.lru_cache(maxsize=SUBSTRATE_RUNTIME_CACHE_SIZE)
816822
def get_runtime_for_version(
817823
self, runtime_version: int, block_hash: Optional[str] = None
818824
) -> Runtime:
@@ -1668,7 +1674,7 @@ def get_metadata(self, block_hash=None) -> MetadataV15:
16681674

16691675
return runtime.metadata_v15
16701676

1671-
@functools.lru_cache(maxsize=512)
1677+
@functools.lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
16721678
def get_parent_block_hash(self, block_hash):
16731679
block_header = self.rpc_request("chain_getHeader", [block_hash])
16741680

@@ -1708,7 +1714,7 @@ def get_storage_by_key(self, block_hash: str, storage_key: str) -> Any:
17081714
"Unknown error occurred during retrieval of events"
17091715
)
17101716

1711-
@functools.lru_cache(maxsize=16)
1717+
@functools.lru_cache(maxsize=SUBSTRATE_RUNTIME_CACHE_SIZE)
17121718
def get_block_runtime_info(self, block_hash: str) -> dict:
17131719
"""
17141720
Retrieve the runtime info of given block_hash
@@ -1718,7 +1724,7 @@ def get_block_runtime_info(self, block_hash: str) -> dict:
17181724

17191725
get_block_runtime_version = get_block_runtime_info
17201726

1721-
@functools.lru_cache(maxsize=512)
1727+
@functools.lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
17221728
def get_block_runtime_version_for(self, block_hash: str):
17231729
"""
17241730
Retrieve the runtime version of the parent of a given block_hash
@@ -1959,7 +1965,7 @@ def _make_rpc_request(
19591965

19601966
return request_manager.get_results()
19611967

1962-
@functools.lru_cache(maxsize=512)
1968+
@functools.lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
19631969
def supports_rpc_method(self, name: str) -> bool:
19641970
"""
19651971
Check if substrate RPC supports given method
@@ -2036,7 +2042,7 @@ def rpc_request(
20362042
else:
20372043
raise SubstrateRequestException(result[payload_id][0])
20382044

2039-
@functools.lru_cache(maxsize=512)
2045+
@functools.lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
20402046
def get_block_hash(self, block_id: int) -> str:
20412047
return self.rpc_request("chain_getBlockHash", [block_id])["result"]
20422048

async_substrate_interface/utils/cache.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ def __init__(
219219
"""
220220
self._inflight: dict[Hashable, asyncio.Future] = {}
221221
self._method = method
222+
self._max_size = max_size
222223
self._cache = LRUCache(max_size=max_size)
223224
self._cache_key_index = cache_key_index
224225

tests/helpers/fixtures.py

Lines changed: 12 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from tests.helpers.fixtures import import_fresh
2+
3+
4+
def test_env_vars(monkeypatch):
5+
monkeypatch.setenv("SUBSTRATE_CACHE_METHOD_SIZE", 10)
6+
monkeypatch.setenv("SUBSTRATE_RUNTIME_CACHE_SIZE", 9)
7+
async_substrate = import_fresh("async_substrate_interface.async_substrate")
8+
asi = async_substrate.AsyncSubstrateInterface("", _mock=True)
9+
assert asi.get_runtime_for_version._max_size == 9
10+
assert asi.get_block_runtime_info._max_size == 9
11+
assert asi.get_parent_block_hash._max_size == 10
12+
assert asi.get_block_runtime_version_for._max_size == 10
13+
assert asi.get_block_hash._max_size == 10
14+
15+
16+
def test_defaults():
17+
async_substrate = import_fresh("async_substrate_interface.async_substrate")
18+
asi = async_substrate.AsyncSubstrateInterface("", _mock=True)
19+
assert asi.get_runtime_for_version._max_size == 16
20+
assert asi.get_block_runtime_info._max_size == 16
21+
assert asi.get_parent_block_hash._max_size == 512
22+
assert asi.get_block_runtime_version_for._max_size == 512
23+
assert asi.get_block_hash._max_size == 512
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from tests.helpers.fixtures import import_fresh
2+
3+
4+
def test_env_vars(monkeypatch):
5+
monkeypatch.setenv("SUBSTRATE_CACHE_METHOD_SIZE", 10)
6+
monkeypatch.setenv("SUBSTRATE_RUNTIME_CACHE_SIZE", 9)
7+
sync_substrate = import_fresh("async_substrate_interface.sync_substrate")
8+
asi = sync_substrate.SubstrateInterface("", _mock=True)
9+
assert asi.get_runtime_for_version.cache_parameters()["maxsize"] == 9
10+
assert asi.get_block_runtime_info.cache_parameters()["maxsize"] == 9
11+
assert asi.get_parent_block_hash.cache_parameters()["maxsize"] == 10
12+
assert asi.get_block_runtime_version_for.cache_parameters()["maxsize"] == 10
13+
assert asi.get_block_hash.cache_parameters()["maxsize"] == 10
14+
15+
16+
def test_defaults():
17+
sync_substrate = import_fresh("async_substrate_interface.sync_substrate")
18+
asi = sync_substrate.SubstrateInterface("", _mock=True)
19+
assert asi.get_runtime_for_version.cache_parameters()["maxsize"] == 16
20+
assert asi.get_block_runtime_info.cache_parameters()["maxsize"] == 16
21+
assert asi.get_parent_block_hash.cache_parameters()["maxsize"] == 512
22+
assert asi.get_block_runtime_version_for.cache_parameters()["maxsize"] == 512
23+
assert asi.get_block_hash.cache_parameters()["maxsize"] == 512

0 commit comments

Comments
 (0)